Wie definiere ich eine Aufzählung mit einem Zeichenfolgenwert?

96

Ich versuche, ein Enumgültiges Trennzeichen zu definieren und hinzuzufügen, das in CSV- oder ähnlichen Dateien verwendet wird. Dann werde ich es an eine ComboBoxals Datenquelle binden, sodass ich beim Hinzufügen oder Entfernen aus der Enum-Definition nichts im Kombinationsfeld ändern muss.

Das Problem ist, wie ich eine Aufzählung mit Zeichenfolgendarstellung definieren kann, etwa:

public enum SeparatorChars{Comma = ",", Tab = "\t", Space = " "}

Saeid Yazdani
quelle
Mögliches Duplikat der Zuordnung
nawfal

Antworten:

112

Sie können nicht - Aufzählungswerte müssen ganzzahlige Werte sein. Sie können entweder Attribute verwenden, um jedem Aufzählungswert einen Zeichenfolgenwert zuzuordnen, oder in diesem Fall, wenn jedes Trennzeichen ein einzelnes Zeichen ist, können Sie einfach den charWert verwenden:

enum Separator
{
    Comma = ',',
    Tab = '\t',
    Space = ' '
}

(BEARBEITEN: Zur Verdeutlichung können Sie charden zugrunde liegenden Typ der Aufzählung nicht erstellen, aber Sie können charKonstanten verwenden, um den Integralwert zuzuweisen, der jedem Aufzählungswert entspricht. Der zugrunde liegende Typ der obigen Aufzählung ist int.)

Dann eine Erweiterungsmethode, falls Sie eine benötigen:

public string ToSeparatorString(this Separator separator)
{
    // TODO: validation
    return ((char) separator).ToString();
}
Jon Skeet
quelle
Char ist in Aufzählungen nicht gültig. Msdn: "Jeder Aufzählungstyp hat einen zugrunde liegenden Typ, der ein beliebiger ganzzahliger Typ außer char sein kann."
Dowhilefor
8
@dowhilefor: Sie können jedoch ein Zeichenliteral für den Wert verwenden , wie in meiner Antwort angegeben. Ich habe es getestet :)
Jon Skeet
Da diese Anforderung für Dateien gilt, benötigt der Benutzer möglicherweise ein CRLF-Trennzeichen. Wird es auch in diesem Fall funktionieren?
Maheep
Danke Jon, zählt das nicht als Zeichen?!
Saeid Yazdani
1
@ShaunLuttin: Aufzählungen sind nur "benannte Zahlen" - eine Zeichenfolgenaufzählung passt also überhaupt nicht zu diesem Modell.
Jon Skeet
82

Soweit ich weiß, dürfen Sie enum keine Zeichenfolgenwerte zuweisen. Sie können eine Klasse mit Zeichenfolgenkonstanten erstellen.

public static class SeparatorChars
{
    public static String Comma { get { return ",";} } 
    public static String Tab { get { return "\t,";} } 
    public static String Space { get { return " ";} } 
}
Maheep
quelle
9
Der Nachteil dieses Ansatzes im Gegensatz zu anderen ist, dass Sie diese nicht aufzählen können, ohne etwas Besonderes zu tun.
Caesay
Dies hilft nicht, bestimmte Werte während der Kompilierungszeit zu erzwingen, da separatores sich nun um eine Zeichenfolge (könnte alles sein) anstelle eines SeparatorTyps mit eingeschränkten gültigen Werten handelt.
ChickenFeet
70

Sie können es erreichen, benötigen aber ein wenig Arbeit.

  1. Definieren Sie eine Attributklasse, die den Zeichenfolgenwert für enum enthält.
  2. Definieren Sie eine Erweiterungsmethode, die den Wert aus dem Attribut zurückgibt. Eg..GetStringValue (dieser Enum-Wert) gibt den Attributwert zurück.
  3. Dann können Sie die Aufzählung wie folgt definieren.
öffentliche Aufzählung Test: int {
    [StringValue ("a")]
    Foo = 1,
    [StringValue ("b")]
    Etwas = 2        
}} 
  1. Um den Wert von Attrinbute Test.Foo.GetStringValue () zurückzugewinnen;

Siehe: Aufzählung mit Zeichenfolgenwerten in C #

Amit Rai Sharma
quelle
4
Ich kenne das alte, aber es ist offensichtlich einzigartig und ermöglicht es Ihnen, Aufzählungen in Code und Zeichenfolgenwert in der Datenbank zu verwenden. Erstaunlich
A_kat
1
Noch ein später Kommentar, aber das ist wirklich eine brillante Lösung
Alan
33

Für eine einfache Aufzählung von Zeichenfolgenwerten (oder einem anderen Typ):

public static class MyEnumClass
{
    public const string 
        MyValue1 = "My value 1",
        MyValue2 = "My value 2";
}

Verwendung: string MyValue = MyEnumClass.MyValue1;

Thierry
quelle
1
Dies ist zwar keine Aufzählung, aber ich denke, dies könnte die beste Lösung für das sein, was der Benutzer versucht. Manchmal ist die einfachste Lösung die beste.
Zesty
28

Sie können dies nicht mit Aufzählungen tun, aber Sie können es so machen:

public static class SeparatorChars
{
    public static string Comma = ",";

    public static string Tab = "\t";

    public static string Space = " ";
}
Fischermaen
quelle
1
+1 Obwohl ich denke, dass es die richtige Lösung ist, würde ich den Namen der Klasse oder den Typ in Zeichen ändern. Nur um konsequent zu sein.
Dowhilefor
Danke, können Sie sagen, was comboBox.DataSource = Enum.GetValues(typeof(myEnum));in diesem Fall das Äquivalent sein wird?
Saeid Yazdani
1
@ Sean87: Wenn du das haben willst, würde ich JonSkeets Antwort nehmen.
Fischermaen
Ich denke, das ist fast die richtige Antwort, weil es nicht innerhalb von switch-caseBlöcken verwendet werden kann. Die Felder sollten constin Ordnung sein. Aber es kann immer noch nicht geholfen werden, wenn Sie wollen Enum.GetValues(typeof(myEnum)).
André Santaló
7
Ich würde constanstelle von verwenden static. Konstanten sind sowohl schreibgeschützt als auch statisch und können in Konstruktoren nicht zugewiesen werden (es sei denn, es handelt sich um schreibgeschützte Felder).
Olivier Jacot-Descombes
12

Sie können nicht, weil enum nur auf einem primitiven numerischen Typ basieren kann. Sie könnten Dictionarystattdessen versuchen, a zu verwenden:

Dictionary<String, char> separators = new Dictionary<string, char>
{
    {"Comma", ','}, 
    {"Tab",  '\t'}, 
    {"Space", ' '},
};

Alternativ können Sie ein verwenden Dictionary<Separator, char>oder Dictionary<Separator, string>wo Separatorein normaler Enum:

enum Separator
{
    Comma,
    Tab,
    Space
}

Das wäre etwas angenehmer, als die Saiten direkt zu handhaben.

Adam
quelle
11

Eine Klasse, die das Enum-Verhalten emuliert, aber stringanstelle von verwendet, intkann wie folgt erstellt werden ...

public class GrainType
{
    private string _typeKeyWord;

    private GrainType(string typeKeyWord)
    {
        _typeKeyWord = typeKeyWord;
    }

    public override string ToString()
    {
        return _typeKeyWord;
    }

    public static GrainType Wheat = new GrainType("GT_WHEAT");
    public static GrainType Corn = new GrainType("GT_CORN");
    public static GrainType Rice = new GrainType("GT_RICE");
    public static GrainType Barley = new GrainType("GT_BARLEY");

}

Verwendung...

GrainType myGrain = GrainType.Wheat;

PrintGrainKeyword(myGrain);

dann...

public void PrintGrainKeyword(GrainType grain) 
{
    Console.Writeline("My Grain code is " + grain.ToString());   // Displays "My Grain code is GT_WHEAT"
}
colmde
quelle
Das einzige, was Sie GrainType myGrain = "GT_CORN"zum Beispiel nicht können.
Colmde
Sie könnten, wenn Sie den Operator überschreiben
SSX-SL33PY
8

Es ist etwas spät für eine Antwort, aber vielleicht hilft es jemandem in Zukunft. Ich fand es einfacher, struct für diese Art von Problem zu verwenden.

Das folgende Beispiel kopiert einen eingefügten Teil aus dem MS-Code:

namespace System.IdentityModel.Tokens.Jwt
{
    //
    // Summary:
    //     List of registered claims from different sources http://tools.ietf.org/html/rfc7519#section-4
    //     http://openid.net/specs/openid-connect-core-1_0.html#IDToken
    public struct JwtRegisteredClaimNames
    {
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Actort = "actort";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Typ = "typ";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Sub = "sub";
        //
        // Summary:
        //     http://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout
        public const string Sid = "sid";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Prn = "prn";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Nbf = "nbf";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string Nonce = "nonce";
        //
        // Summary:
        //     http://tools.ietf.org/html/rfc7519#section-4
        public const string NameId = "nameid";

    }
}
Suchoss
quelle
Könnten Sie bitte erklären, warum dieser Ansatz besser ist als die Verwendung einer Klasse?
Gerardo Grignoli
@GerardoGrignoli Ich weiß nicht genau, warum sie in MS für diese Art von Dingen struct anstelle von class verwenden. Ich habe nicht einmal versucht, es herauszufinden, da dies für mich perfekt funktioniert. Vielleicht versuchen Sie, hier auf dem Stapel eine Frage zu stellen ...
suchoss
5

Für Leute, die hier ankommen und nach einer Antwort auf eine allgemeinere Frage suchen, können Sie das statische Klassenkonzept erweitern, wenn Ihr Code wie ein Code aussehen soll enum.

Der folgende Ansatz funktioniert, wenn Sie das enum namesgewünschte nicht fertiggestellt haben und enum valuesdie stringDarstellung des enam name; Verwenden nameof()Sie diese Option, um das Refactoring zu vereinfachen.

public static class Colours
{
    public static string Red => nameof(Red);
    public static string Green => nameof(Green);
    public static string Blue => nameof(Blue);
}

Dadurch wird die Absicht einer Aufzählung mit Zeichenfolgenwerten (z. B. dem folgenden Pseudocode) erreicht:

public enum Colours
{
    "Red",
    "Green",
    "Blue"
}
Zodman
quelle
5

Vielleicht ist es zu spät, aber hier geht es.

Wir können das Attribut EnumMember verwenden, um Enum-Werte zu verwalten.

public enum EUnitOfMeasure
{
    [EnumMember(Value = "KM")]
    Kilometer,
    [EnumMember(Value = "MI")]
    Miles
}

Auf diese Weise ist der Ergebniswert für EUnitOfMeasure KM oder MI. Dies ist auch in der Antwort von Andrew Whitaker zu sehen .

Javier Contreras
quelle
4

Ich habe eine Basisklasse zum Erstellen von Aufzählungen mit Zeichenfolgen in .NET erstellt. Es ist nur eine C # -Datei, die Sie kopieren und in Ihre Projekte einfügen oder über das NuGet-Paket StringEnum installieren können .

Verwendung:

///<completionlist cref="HexColor"/> 
class HexColor : StringEnum<HexColor>
{
    public static readonly HexColor Blue = New("#FF0000");
    public static readonly HexColor Green = New("#00FF00");
    public static readonly HexColor Red = New("#000FF");
}

Eigenschaften

  • Ihre StringEnum ähnelt einer regulären Aufzählung:
    // Static Parse Method
    HexColor.Parse("#FF0000") // => HexColor.Red
    HexColor.Parse("#ff0000", caseSensitive: false) // => HexColor.Red
    HexColor.Parse("invalid") // => throws InvalidOperationException

    // Static TryParse method.
    HexColor.TryParse("#FF0000") // => HexColor.Red
    HexColor.TryParse("#ff0000", caseSensitive: false) // => HexColor.Red
    HexColor.TryParse("invalid") // => null

    // Parse and TryParse returns the preexistent instances
    object.ReferenceEquals(HexColor.Parse("#FF0000"), HexColor.Red) // => true

    // Conversion from your `StringEnum` to `string`
    string myString1 = HexColor.Red.ToString(); // => "#FF0000"
    string myString2 = HexColor.Red; // => "#FF0000" (implicit cast)
  • Intellisense schlägt den Namen der Aufzählung vor, wenn die Klasse mit dem XML-Kommentar versehen ist <completitionlist>. (Funktioniert sowohl in C # als auch in VB): dh

Intellisense-Demo

Installation

Entweder:

  • Installieren Sie das neueste StringEnum NuGet-Paket, das darauf basiert, .Net Standard 1.0dass es auf .Net Core> = 1.0, .Net Framework> = 4.5, Mono> = 4.6 usw. ausgeführt wird.
  • Oder fügen Sie die folgende StringEnum-Basisklasse in Ihr Projekt ein. ( neueste Version )
    public abstract class StringEnum<T> : IEquatable<T> where T : StringEnum<T>, new()
    {
        protected string Value;
        private static IList<T> valueList = new List<T>();
        protected static T New(string value)
        {
            if (value == null)
                return null; // the null-valued instance is null.

            var result = new T() { Value = value };
            valueList.Add(result);
            return result;
        }

        public static implicit operator string(StringEnum<T> enumValue) => enumValue.Value;
        public override string ToString() => Value;

        public static bool operator !=(StringEnum<T> o1, StringEnum<T> o2) => o1?.Value != o2?.Value;
        public static bool operator ==(StringEnum<T> o1, StringEnum<T> o2) => o1?.Value == o2?.Value;

        public override bool Equals(object other) => this.Value.Equals((other as T)?.Value ?? (other as string));
        bool IEquatable<T>.Equals(T other) => this.Value.Equals(other.Value);
        public override int GetHashCode() => Value.GetHashCode();

        /// <summary>
        /// Parse the <paramref name="value"/> specified and returns a valid <typeparamref name="T"/> or else throws InvalidOperationException.
        /// </summary>
        /// <param name="value">The string value representad by an instance of <typeparamref name="T"/>. Matches by string value, not by the member name.</param>
        /// <param name="caseSensitive">If true, the strings must match case sensitivity.</param>
        public static T Parse(string value, bool caseSensitive = false)
        {
            var result = TryParse(value, caseSensitive);
            if (result == null)
                throw new InvalidOperationException((value == null ? "null" : $"'{value}'") + $" is not a valid {typeof(T).Name}");

            return result;
        }

        /// <summary>
        /// Parse the <paramref name="value"/> specified and returns a valid <typeparamref name="T"/> or else returns null.
        /// </summary>
        /// <param name="value">The string value representad by an instance of <typeparamref name="T"/>. Matches by string value, not by the member name.</param>
        /// <param name="caseSensitive">If true, the strings must match case sensitivity.</param>
        public static T TryParse(string value, bool caseSensitive = false)
        {
            if (value == null) return null;
            if (valueList.Count == 0) System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle); // force static fields initialization
            var field = valueList.FirstOrDefault(f => f.Value.Equals(value,
                    caseSensitive ? StringComparison.Ordinal
                                  : StringComparison.OrdinalIgnoreCase));
            // Not using InvariantCulture because it's only supported in NETStandard >= 2.0

            if (field == null)
                return null;

            return field;
        }
    }
  • Newtonsoft.JsonKopieren Sie zur Unterstützung der Serialisierung stattdessen diese erweiterte Version. StringEnum.cs

Ich erkannte nach der Tatsache, dass dieser Code Bens Antwort ähnlich ist. Ich habe es aufrichtig von Grund auf neu geschrieben. Ich denke jedoch, dass es einige Extras hat, wie den <completitionlist>Hack, die resultierende Klasse sieht eher aus wie eine Aufzählung, keine Verwendung von Reflexion über Parse (), das NuGet-Paket und Repo, wo ich hoffentlich eingehende Probleme und Feedback ansprechen werde.

Gerardo Grignoli
quelle
3

Aufbauend auf einigen der Antworten hier habe ich eine wiederverwendbare Basisklasse implementiert, die das Verhalten einer Aufzählung nachahmt, jedoch stringals zugrunde liegenden Typ. Es unterstützt verschiedene Operationen, einschließlich:

  1. eine Liste möglicher Werte erhalten
  2. Konvertieren in einen String
  3. Vergleich mit anderen Fällen über .Equals, ==und!=
  4. Konvertierung zu / von JSON mit einem JSON.NET JsonConverter

Dies ist die Basisklasse in ihrer Gesamtheit:

public abstract class StringEnumBase<T> : IEquatable<T>
    where T : StringEnumBase<T>
{
    public string Value { get; }

    protected StringEnumBase(string value) => this.Value = value;

    public override string ToString() => this.Value;

    public static List<T> AsList()
    {
        return typeof(T)
            .GetProperties(BindingFlags.Public | BindingFlags.Static)
            .Where(p => p.PropertyType == typeof(T))
            .Select(p => (T)p.GetValue(null))
            .ToList();
    }

    public static T Parse(string value)
    {
        List<T> all = AsList();

        if (!all.Any(a => a.Value == value))
            throw new InvalidOperationException($"\"{value}\" is not a valid value for the type {typeof(T).Name}");

        return all.Single(a => a.Value == value);
    }

    public bool Equals(T other)
    {
        if (other == null) return false;
        return this.Value == other?.Value;
    }

    public override bool Equals(object obj)
    {
        if (obj == null) return false;
        if (obj is T other) return this.Equals(other);
        return false;
    }

    public override int GetHashCode() => this.Value.GetHashCode();

    public static bool operator ==(StringEnumBase<T> a, StringEnumBase<T> b) => a?.Equals(b) ?? false;

    public static bool operator !=(StringEnumBase<T> a, StringEnumBase<T> b) => !(a?.Equals(b) ?? false);

    public class JsonConverter<T> : Newtonsoft.Json.JsonConverter
        where T : StringEnumBase<T>
    {
        public override bool CanRead => true;

        public override bool CanWrite => true;

        public override bool CanConvert(Type objectType) => ImplementsGeneric(objectType, typeof(StringEnumBase<>));

        private static bool ImplementsGeneric(Type type, Type generic)
        {
            while (type != null)
            {
                if (type.IsGenericType && type.GetGenericTypeDefinition() == generic)
                    return true;

                type = type.BaseType;
            }

            return false;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JToken item = JToken.Load(reader);
            string value = item.Value<string>();
            return StringEnumBase<T>.Parse(value);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value is StringEnumBase<T> v)
                JToken.FromObject(v.Value).WriteTo(writer);
        }
    }
}

Und so würden Sie Ihre "String-Aufzählung" implementieren:

[JsonConverter(typeof(JsonConverter<Colour>))]
public class Colour : StringEnumBase<Colour>
{
    private Colour(string value) : base(value) { }

    public static Colour Red => new Colour("red");
    public static Colour Green => new Colour("green");
    public static Colour Blue => new Colour("blue");
}

Welches könnte so verwendet werden:

public class Foo
{
    public Colour colour { get; }

    public Foo(Colour colour) => this.colour = colour;

    public bool Bar()
    {
        if (this.colour == Colour.Red || this.colour == Colour.Blue)
            return true;
        else
            return false;
    }
}

Ich hoffe jemand findet das nützlich!

Ben
quelle
2

Nun, zuerst versuchen Sie, Zeichenfolgen und keine Zeichen zuzuweisen, auch wenn es sich nur um ein Zeichen handelt. Verwenden Sie ',' anstelle von ",". Als nächstes nehmen Aufzählungen nur ganzzahlige Typen an, ohne dass charSie den Unicode-Wert verwenden könnten, aber ich würde Ihnen dringend raten, dies nicht zu tun. Wenn Sie sicher sind, dass diese Werte in verschiedenen Kulturen und Sprachen gleich bleiben, würde ich eine statische Klasse mit const-Zeichenfolgen verwenden.

dowhilefor
quelle
2

Während es wirklich nicht möglich ist, ein charoder ein stringals Basis für eine Aufzählung zu verwenden, denke ich, dass dies nicht das ist, was Sie wirklich gerne tun.

Wie Sie bereits erwähnt haben, möchten Sie eine Reihe von Möglichkeiten haben und eine Zeichenfolgendarstellung davon in einem Kombinationsfeld anzeigen. Wenn der Benutzer eine dieser Zeichenfolgendarstellungen auswählt, möchten Sie die entsprechende Aufzählung erhalten. Und das ist möglich:

Zuerst müssen wir einen String mit einem Enum-Wert verknüpfen. Dies kann durch die durchgeführt werden , DescriptionAttributewie es beschrieben wird hier oder hier .

Jetzt müssen Sie eine Liste mit Aufzählungswerten und entsprechenden Beschreibungen erstellen. Dies kann mit der folgenden Methode erfolgen:

/// <summary>
/// Creates an List with all keys and values of a given Enum class
/// </summary>
/// <typeparam name="T">Must be derived from class Enum!</typeparam>
/// <returns>A list of KeyValuePair&lt;Enum, string&gt; with all available
/// names and values of the given Enum.</returns>
public static IList<KeyValuePair<T, string>> ToList<T>() where T : struct
{
    var type = typeof(T);

    if (!type.IsEnum)
    {
        throw new ArgumentException("T must be an enum");
    }

    return (IList<KeyValuePair<T, string>>)
            Enum.GetValues(type)
                .OfType<T>()
                .Select(e =>
                {
                    var asEnum = (Enum)Convert.ChangeType(e, typeof(Enum));
                    return new KeyValuePair<T, string>(e, asEnum.Description());
                })
                .ToArray();
}

Jetzt haben Sie eine Liste der Schlüsselwertpaare aller Aufzählungen und deren Beschreibung. Weisen wir dies einfach als Datenquelle für ein Kombinationsfeld zu.

var comboBox = new ComboBox();
comboBox.ValueMember = "Key"
comboBox.DisplayMember = "Value";
comboBox.DataSource = EnumUtilities.ToList<Separator>();

comboBox.SelectedIndexChanged += (sender, e) =>
{
    var selectedEnum = (Separator)comboBox.SelectedValue;
    MessageBox.Show(selectedEnum.ToString());
}

Der Benutzer sieht alle Zeichenfolgendarstellungen der Aufzählung und in Ihrem Code erhalten Sie den gewünschten Aufzählungswert.

Oliver
quelle
0

Wir können die Aufzählung nicht als Zeichenfolgentyp definieren. Die genehmigten Typen für eine Aufzählung sind Byte, Sbyte, Kurz, Ushort, Int, Uint, Long oder Ulong.

Wenn Sie weitere Informationen zur Aufzählung benötigen, folgen Sie bitte dem folgenden Link. Dieser Link hilft Ihnen, die Aufzählung zu verstehen. Aufzählung

@ narendras1414

Narendra1414
quelle
0

Für mich geht das..

   public class ShapeTypes
    {
        private ShapeTypes() { }
        public static string OVAL
        {
            get
            {
                return "ov";
            }
            private set { }
        }

        public static string SQUARE
        {
            get
            {
                return "sq";
            }
            private set { }
        }

        public static string RECTANGLE
        {
            get
            {
                return "rec";
            }
            private set { }
        }
    }
Rakesh Kr
quelle
0

Was ich kürzlich begonnen habe, ist die Verwendung von Tupeln

public static (string Fox, string Rabbit, string Horse) Animals = ("Fox", "Rabbit", "Horse");
...
public static (string Comma, string Tab, string Space) SeparatorChars = (",", "\t", " ");
Luke T O'Brien
quelle
-1

Aufzählungsklasse

 public sealed class GenericDateTimeFormatType
    {

        public static readonly GenericDateTimeFormatType Format1 = new GenericDateTimeFormatType("dd-MM-YYYY");
        public static readonly GenericDateTimeFormatType Format2 = new GenericDateTimeFormatType("dd-MMM-YYYY");

        private GenericDateTimeFormatType(string Format)
        {
            _Value = Format;
        }

        public string _Value { get; private set; }
    }

Enumaration Consuption

public static void Main()
{
       Country A = new Country();

       A.DefaultDateFormat = GenericDateTimeFormatType.Format1;

      Console.ReadLine();
}
Mark Macneil Bikeio
quelle