I try to select the syntax for initializing the attached properties in my CsConsoleFormat library. Attached properties perform the same role as WPF.

Here are the options drawn. For various reasons, none like it.

  1. Indexer (ak.a dictionary initializer):

    var a = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, [Element.FooProperty] = 2.1, [Element.BazProperty] = Guid.NewGuid() }; a.WritePropertyValues(); 

    Pros:

    1. It looks pretty, almost like initializing normal properties.
    2. Combined with object initializer at the same level.
    3. It works not only initialization, but also reading and writing.
    4. Avalonia UI uses similar syntax for binding.

    Minuses:

    1. One, but bold: indexers can not be generalized, so no type checking, everywhere object . And not only verification, but also conversion is lost.
  2. Collection initializer with two arguments:

     var b = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { { Element.FooProperty, 2 }, { Element.BarProperty, "Hello!" } } }; b.WritePropertyValues(); 

    Pros:

    1. Strongly typed.
    2. Can be combined with an indexer for reading and writing.

    Minuses:

    1. Initializers of collections and objects are not combined, it is necessary to allocate a separate level.
    2. It looks doubtful because of the forest of curly braces: at the end of their expression there are already three of them.
    3. If you add an indexer for reading and writing, you get a jumble: in one place it is typed, in the other not.
  3. A single-argument collection initializer in conjunction with an operator:

     var c = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty == 10, Element.BazProperty == Guid.NewGuid() } }; c.WritePropertyValues(); 

    Pros:

    1. Strongly typed.
    2. The syntax is moderately brief and moderately pleasant.
    3. Can be combined with an indexer for reading and writing.

    Minuses:

    1. The assignment operator does not overload, you have to overload the lowest priority binary operator, and it is already high enough to spoil some expressions ( Element.BoolProperty == a == b ).
    2. Equality for imitation assignment is not the most logical move. I do not want to be in the role of the "inventor" of the >> operator in C ++. However, in Avalonia UI allow to play with the operators, why not me.
    3. If you add an indexer for reading and writing, you get a jumble: in one place it is typed, in the other not.
  4. Good old fluent :

     var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .Set(Element.FooProperty, 1337).Set(Element.BarProperty, "World!"); d.WritePropertyValues(); 

    Pros:

    1. Strongly typed.
    2. Can be combined with the symmetric Get method.
    3. If combined with Get , then the operation is symmetrical and uniform.

    Minuses:

    1. The syntax is horrible and terrible, if you want to use both ordinary and attached properties (to transfer all properties to a fluent is not an option).

Code that implements all the syntaxes described above:

 internal class Program { private static void Main() { var a = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, [Element.FooProperty] = 2.1, [Element.BazProperty] = Guid.NewGuid() }; a.WritePropertyValues(); var b = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { { Element.FooProperty, 2 }, { Element.BarProperty, "Hello!" } } }; b.WritePropertyValues(); var c = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty == 10, Element.BazProperty == Guid.NewGuid() } }; c.WritePropertyValues(); var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .Set(Element.FooProperty, 1337).Set(Element.BarProperty, "World!"); d.WritePropertyValues(); Console.ReadKey(); } } internal class Element { public static readonly Property<int> FooProperty = Property.Register("Foo", 1); public static readonly Property<string> BarProperty = Property.Register("Bar", "a"); public static readonly Property<Guid> BazProperty = Property.Register("Baz", Guid.Empty); private readonly Dictionary<Property, object> _properties = new Dictionary<Property, object>(); public int Oops { get; set; } public int I { get; set; } public int Did { get; set; } public int It { get; set; } public int Again { get; set; } public Values Values { get; } public Element() { Values = new Values(this); } public object this[Property property] { get => _properties.TryGetValue(property, out object value) ? value : property.DefaultValueUntyped; set => _properties[property] = value; } public Element Set<T>(Property<T> prop, T v) { _properties[prop] = v; return this; } public void WritePropertyValues() { foreach (KeyValuePair<Property, object> property in _properties) Console.WriteLine($"{property.Key.Type.Name} {property.Key.Name} = {property.Value} (default: {property.Key.DefaultValueUntyped})"); Console.WriteLine(); } } internal class Values : IEnumerable { private readonly Element _element; public Values(Element element) => _element = element; public void Add<T>(Property<T> prop, T v) => _element[prop] = v; public void Add<T>(PropertyValue<T> pv) => _element[pv.Property] = pv.Value; IEnumerator IEnumerable.GetEnumerator() => null; } internal abstract class Property { public string Name { get; } public object DefaultValueUntyped { get; } public abstract Type Type { get; } protected Property(string name, object defaultValueUntyped) { Name = name; DefaultValueUntyped = defaultValueUntyped; } public static Property<T> Register<T>(string name, T defaultValue) => new Property<T>(name, defaultValue); } internal class Property<T> : Property { public T DefaultValue => (T)DefaultValueUntyped; public override Type Type => typeof(T); internal Property(string name, T defaultValue) : base(name, defaultValue) { } public static PropertyValue<T> operator ==(Property<T> property, T value) => new PropertyValue<T>(property, value); public static PropertyValue<T> operator !=(Property<T> property, T value) => default; } internal struct PropertyValue<T> { public Property Property { get; set; } public T Value { get; set; } public PropertyValue(Property property, T value) { Property = property; Value = value; } } 

Perhaps, I am losing some more convenient way of seeing? Are there any other alternatives? Preferably strongly typed and with short syntax.

Opinions about the options described above are also welcome.

    2 answers 2

    Personally, I like the fluent option - the fact that it is separated from the usual properties, even a plus (flies separately, cutlets separately). But if you want to get the code shorter, then you have to reinvent the conventions. I thought about anonymous classes, as they are used in asp.net mvc for example.

     var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .AddProperties<Element>(new { FooProperty = 10, BarProperty = "alkdjalkds", BazProperty = Guid.NewGuid() }); 

    If you really need strong typing, you can do something like this

     public static class Ext { internal static T Value<T>(this Property<T> prop, T value) { return value; // тут можно придумать что то типа AttchedPropertyValue<T>, который можно создать только отсюда } } 

    Then

     .AddProperties<Element>(new { FooProperty = Element.FooProperty.Value(10), BarProperty = Element.BarProperty.Value("alkdjalkds"), BazProperty = Element.BazProperty.Value(Guid.NewGuid()) }); 

    Well, and consider it all like that

     public Element AddProperties<T>(object b) { var sourceType = typeof(T); foreach (var p in b.GetType().GetProperties()) { var fieldInfo = sourceType.GetField(p.Name, BindingFlags.Public | BindingFlags.Static); if (fieldInfo == null) throw new ArgumentException("bla bla"); var fieldValue = fieldInfo.GetValue(null) as Property; var value = p.GetValue(b); if (fieldInfo.FieldType.IsGenericType) if (value.GetType()!=fieldInfo.FieldType.GetGenericArguments()[0]) throw new ArgumentException("bla bla bla"); _properties[fieldValue] = value; } return this; } 
    • I do not accidentally try to place the usual and attached properties nearby. The fact is that both those and others are usually logically followed before the description of the sub-elements. The problem is described in more detail in the "first part" of the question . I implemented the approach proposed in that answer with a collection initializer for subitems, so if you move the initialization of the attached properties to the method, then the final sequence will result in "properties, subelements, attached properties", which is unnatural. - Athari
    • Your typing is not strict, you have everything dynamic, and even more dynamic than me. I want all the checks to be performed at compilation, and you even remove the property name check at runtime. And code completion is lost. This is the type of argument I can check in the indexer. - Athari
    • @Athari about typing I, of course, meant specifying typed values ​​of properties (and not a set of properties). And it's not very clear to me, how do you want to have a strict typification of everything at compile time when it comes to the dictionary of attached properties? In my opinion, the dynamics here itself suggests itself. But I missed your first question, yes. I read at my leisure - tym32167
    • Attached properties do not appear dynamically in runtime, they already exist, are described in static properties, are typed from all sides, just their values ​​need to be "attached" not to the elements in whose class they are defined. (That in my example everything is pushed into one Element is a simplification.) For example, a Grid container can define attached Grid.Column and Grid.Row properties so that child elements can specify in which cell they should be placed. Properties are defined in the Grid , and values ​​can be thrust into any child element that knows nothing about the Grid . - Athari
    • Yes, I know what it is. I mean, you want to have a set of properties, and to treat them as if they are properties of an object - in my opinion, if it is doable (which I doubt), it is a very difficult task. And if you look at WPF, then there I suspect the dynamics on the dynamics at all. I mean, your task most likely does not have a solution that is statically typed in the full sense of the word, and you still have to make some compromises. However, after reading your first question, I agree that the fluent will be out of place there. - tym32167

    I also have an option with Value :

     internal class Property<T> : Property { // ... public PropertyValue<T> Value(T t) => new PropertyValue<T>(this, t); } 

    and syntax

     var e = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty.Value(10), Element.BazProperty.Value(Guid.NewGuid()) } }; 

    Strong typing available. The syntax is not very.


    Another option with strong typing, due to a more complex definition of attached property (which, however, can be taken out in snippet):

     var f = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { (Element.Foo)10, (Element.Baz)Guid.NewGuid() } }; 

    Achieved by the following code change:

     interface IPropertyValue<T> { Property Property { get; } T Value { get; } } internal struct PropertyValue<T> : IPropertyValue<T> { // остальное как было } internal class Values : IEnumerable { private readonly Element _element; public Values(Element element) => _element = element; public void Add<T>(Property<T> prop, T v) => _element[prop] = v; // заменили на интерфейс public void Add<T>(IPropertyValue<T> pv) => _element[pv.Property] = pv.Value; IEnumerator IEnumerable.GetEnumerator() => null; } 

    And the definition:

     internal class Element { public static readonly Property<int> FooProperty = Property.Register("Foo", 1); public struct Foo : IPropertyValue<int> { public Property Property => Element.FooProperty; public int Value { get; set; } public static explicit operator Foo(int value) => new Foo() { Value = value }; } public static readonly Property<string> BarProperty = Property.Register("Bar", "a"); public struct Bar : IPropertyValue<string> { public Property Property => Element.BarProperty; public string Value { get; set; } public static explicit operator Bar(string value) => new Bar() { Value = value }; } public static readonly Property<Guid> BazProperty = Property.Register("Baz", Guid.Empty); public struct Baz : IPropertyValue<Guid> { public Property Property => Element.BazProperty; public Guid Value { get; set; } public static explicit operator Baz(Guid value) => new Baz() { Value = value }; } private readonly Dictionary<Property, object> _properties = new Dictionary<Property, object>(); // дальше как было 
    • I did not offer this, because, in fact, it turns out that FooProperty can be added at least 10 times in one initializer - tym32167
    • @ tym32167: This is yes. Is it possible to deal with this? - VladD
    • @ tym32167 A curious remark about reassignment. Just in case, added a check in todo , so as not to forget. But in general, this problem is less critical than postponing a fall due to assignment a la int a = "foo" until execution time. - Athari
    • @VladD Syntactically not the worst option: there is no forest of brackets, the alignment is even. It's a pity that neither operator() nor operator= can be overloaded in C #. - Athari
    • @VladD By the way, have you seen my previous question ? In principle, the topic is not yet closed, I have not released version 1.0 yet, so I am open to suggestions. - Athari