Introduction

I am working on the CsConsoleFormat library, which prints hierarchical documents to the console. XAML and APIs are akin to LINQ to XML for building documents. I need a clean, short, understandable API, since the main goal of the library is to make complex formatting simple. Moreover, the API should, whenever possible, encourage the writing of readable code .

The System.Xml.Linq.XElement class has a constructor that takes a params object[] content argument and applies the following transformations to it: collapse sequences into elements, skip null, convert strings to text nodes, add elements and attributes as is, convert everything else to rows.

Problem

My library relies on the same approach with params object[] content , but instead of the constructor, the AddChildren method is AddChildren . The reasons for this are as follows:

  1. Constructors with arguments like params object[] content may seem unnatural to those working with XAML. The problem becomes more acute, if I add meaningful constructors, I have to add this argument to all constructors.

  2. Unlike XElement , my elements have full-fledged properties that are conveniently written in the initializer. And if new Span("Yellow") { Color = Yellow } looks normal, then when a hierarchy appears, the logical sequence is lost:

     new Document( new Span("Title") { Color = White }, new Grid() { // ... очень длинный документ } new Span("Footer") ) { Color = Yellow, } 

    Suddenly, after reading the entire document, you notice that all the elements inherited the yellow color from the root element. Moreover, the library supports attached properties (for example, Grid.Column ), which can be set only after calling the constructor (using the Set extension method).

    When using the AddChildren method AddChildren logical sequence is preserved:

     new Document { Color = Yellow, }.AddChildren( new Span("Title") { Color = White }, new Grid { /* ... */ } .AddChildren( // ... очень длинный документ ) new Span("Footer") ) 

    However, if only AddChildren , then a short code like this:

     new Div(DateTime.Now), new Div(DateTime.Now) { TextAlign = Left }, 

    becomes more verbose:

     new Div().AddChildren(DateTime.Now), new Div { TextAlign = Left }.AddChildren(DateTime.Now), 

Questions

  1. Is there an approach that combines as many advantages as possible and avoids as many of the above as possible?

  2. If I decide to include both constructors and AddChildren , is there any way to force / motivate the encoder to write code in accordance with the style described above (designer for single-line children, method for complex content)? Roslyn code analyzers for this fit?

Notes

  1. I know of an even simpler API - extension methods a la npm / colors . However, it seems that this method is applicable only in a narrow subset of cases, so the problem remains.

Cross-post: API design issues - Implementing API to XML with elements having properties .

1 answer 1

If the goal is to do everything as in Linq to XML, then the properties of the elements are an obvious analogue of attributes. So, you can do with them the same way as with attributes:

 new Document( new ColorProperty(Yellow), new Span("Title", new ColorProperty(White)), new Grid() { // ... очень длинный документ } new Span("Footer") ) 

However, I do not consider Linq to XML to be the benchmark for the intelligibility of the API - and therefore it is not at all necessary to do everything as done there. In the end, in C # there are more typed options to do the same:

 new Document { Color = Yellow, Children = { new Span("Title") { Color = White }, new Grid { }, "Footer", }, } 

Attached properties can be managed in a similar way:

 new Span("Foo") { AttachedProperties = { { Grid.ColumnProperty, 1 }, }, } 

Recall that in order to use the Collection Initializer in C #, the type must have an Add method and implement an IEnumerable interface - these two requirements are not related to each other: you can implement an arbitrary number of overloads of the Add method with any number of parameters and any types of these parameters.

  • The first way looks ugly, to be honest: a lot of types, no visibility. 😆 For the second: I now have Children type ElementCollection : ElementCollection<Element> : Collection<Element> . You offer me to make a Collection<Object> , which is hard to call “typed”. In addition, there are attached properties that will fly to the end anyway: new Element(/*...*/).Set(Grid.ColumnProperty, 1) . - Athari
  • @Athari is not necessary to do Collection<Object> - rather overloaded Add method. - Pavel Mayorov
  • I like the idea with collection initializers. A collection of elements already implements explicit IList.Add (albeit with a different implementation and to support XAML ...), and for attached properties I still consider the empty variant for adding properties (the internal presentation of the collection of attached properties is too unfriendly, and dragging friendly methods into a wrapper - just complicate the syntax). This thing after all in C # and VB is supported equally in such type (IEnumerable + Add)? - Athari
  • @Athari I don’t know anything about VB - Pavel Mayorov
  • I tried. It looks really clean, I like it. Raspberry spoils only the fact that Sharp (well, or dotnet) does not know how generic indexers. Unlike collection initializers, dictionary initializers are combined with object initializers, but rely on indexers, and this<T>(AttachedProperty<T> property, T value) cannot be written, and the attached properties are untyped. The Add<T>(AttachedProperty<T> property, T value) variant Add<T>(AttachedProperty<T> property, T value) typed, implicit type conversions work, but it turns out a forest of parentheses. - Athari