Part 1: Have you tried the new composite controls?

|
Publikováno:

DotVVM 4.0 has introduced a new way of building custom controls called Composite controls. In this article, we’ll share multiple use cases for these controls, and also some useful tips and tricks how to use this concept efficiently.

You can find all the sample code in this GitHub repository.


Composite vs traditional controls

In previous versions of DotVVM, there were two ways of building custom controls – markup controls and code-only controls.

  • The markup controls were useful if you just needed to repeat the same piece of markup on multiple places, with a few trivial substitutions.
  • Code-only controls are the most powerful way of building controls, but the API is quite low-level. You can affect all the phases of the control life-cycle – for example, the control can build its own hierarchy of child controls, set properties on them, respond to some events, or completely rewrite the rendering procedure of the control. However, this showed to be quite complicated for most use cases.

The new concept of composite controls is useful when you need to compose your own high-level control from existing controls or HTML elements. It doesn’t help you with JavaScript interactivity in any way, however it can be used to quickly build a hierarchy of HTML elements or other controls to be rendered. Also, the API contains many helper methods to work with bindings, and also properties which may or may not contain a binding – this “duality” required a lot of code in the traditional approach for building controls.

Simple composite control

One can say that the composite control is “just a function which declares control properties as its parameters, and returns the inside of the control”:

public class ThumbnailPlaceholderComposite : CompositeControl
{
    public static DotvvmControl GetContents(
        [DefaultValue(null)] ValueOrBinding<string> imageUrl,
        int size = 150
    )
    {
        return new HtmlGenericControl("img")
            .SetAttribute("alt", "Placeholder")
            .SetAttribute("src", imageUrl)
            .AddCssStyle("height", size + "px");
    }
}

You can see in the code snippet above that the composite control is declared as a class inheriting from CompositeControl, and contains a static function called GetContents.

This method declares two parameters – imageUrl and size – they are the control properties.

  • The imageUrl parameter is declared as ValueOrBinding<string> type – this tells DotVVM that the property accepts both “static value” or a data-binding expression.
  • The size parameter is just an int (so it cannot contain a binding).
  • Both properties have default value – the ValueOrBinding needs to specify it via the DefaultValue attribute, simpler types as int can use just the default value of the parameter. Both ways are legit.

The method returns DotvvmControl (it can also return IEnumerable<DotvvmControl>) which represents what the control consists of.

  • In our case, we are building the <img> tag.
  • The alt attribute is set to a static value.
  • The src attribute is set to the imageUrl variable which can be either a static value or a binding. If it is a static value, DotVVM will render just src=”value.jpg”. If it is a binding, DotVVM will render data-bind=”attr: { src: SomeViewModelProperty }” which will evaluate on the client-side and sets the attribute via JavaScript.
  • We are also adding a CSS style – it will also either render the style attribute, or builds it using the Knockout data-bind attribute on the client.

The control can be used in the markup as usual:

<cc:ThumbnailPlaceholderComposite ImageUrl="{value: AuthorImageUrl}" />

Working with data-bindings

DotVVM 4.0 added several extension methods that provide a fluent API to build control hierarchies:

  • SetAttribute for settings HTML attributes
  • SetProperty for setting DotVVM properties on controls
  • AddCssStyle for composing the style attribute
  • AddCssClass for composing CSS classes
  • AppendChidlren for appending content inside some control or element

You can chain these methods, so many times you’ll start your composite control with the return clause followed by a long expression which builds the entire control tree.

Most of the methods can with with ValueOrBinding<T> type, which is crucial – it allows you to have the same code for the case where a static value is passed to the control property as well for the case where the property contains a binding.

Sometimes, you need to use the binding in some more complex expression. DotVVM adds the Select method on the ValueOrBinding<T> which allows to project the binding to some other expression:

.SetAttribute("src", imageUrl.Select(s => string.IsNullOrEmpty(s) ? $"/identicon/{size}" : s))

The logic is a bit similar to the LINQ Select method – you pass a lambda expression which gets the value or binding and returns a new expression. In the code snippet above, we are checking whether the imageUrl is empty or not. If it is, we are building a custom URL /idendicon/{size} which will generate some random image of the specified size.

This allows our control to render a random image when the imageUrl is not present. If it is specified, its value will be used for the src attribute of the image.

Working with templates

In the ThumbnailListComposite control in our sample app, we want to render thumbnails for all objects in a collection. The control should be universal so we don’t know anything about the objects in the collection – it can even be a collection of strings with just the URLs of images.

That’s why the control has the DataSource property typed as IValueBinding<IEnumerable>. The IValueBinding means that the property accepts only data-binding expression – it doesn’t make much sense to work with “hard-coded” collections specified directly in the page markup.

Also, there is ItemImageUrlBinding property which expects a data-binding expression which will indicate how to get the ImageUrl property from the item of the DataSource. If the DataSource will be just a collection of strings, then the expression {value: _this} will be specified in this property. If it is a collection of objects, then the expression can be for example {value: ProfileImageUrl}.

public class ThumbnailListComposite : CompositeControl
{
    public static IEnumerable<DotvvmControl> GetContents(
        [DefaultValue("Photo list")] ValueOrBinding<string> title,

        IValueBinding<IEnumerable> dataSource,

        [ControlPropertyBindingDataContextChange("DataSource", order: 0), CollectionElementDataContextChange(order: 1)] 
        IValueBinding<string> itemImageUrlBinding)
    {
        yield return new HtmlGenericControl("h3")
            .SetProperty(HtmlGenericControl.InnerTextProperty, title);

        yield return new Repeater() 
            {
                ItemTemplate = new DelegateTemplate(_ => 
                    new ThumbnailPlaceholderComposite()
                        .SetProperty("ImageUrl", itemImageUrlBinding)
                        .SetProperty("Size", new ValueOrBinding<int>(200)))
            }
            .SetProperty(r => r.DataSource, dataSource);
    }
}

You can see that the method returns IEnumerable<DotvvmControl> – it renders two components – the title and the repeater with thumbnails generated from the collection.

The ItemImageUrlBinding property specifies two attributes which tell DotVVM in which binding context the data-binding expression will work. The problem here is that when you use the control, your data context is the page viewmodel (which contains the People collection):

<cc:ThumbnailListComposite Title="Composite" 
                           DataSource="{value: People}" 
                           ItemImageUrlBinding="{value: ImageUrl}" />

The page viewmodel however doesn’t have the ImageUrl – this property is declared on the Person class whose instances are inside the People collection. For every binding expression, DotVVM needs so called DataContextStack. You can imagine it as a bottom-to-top list of types of the data context. DotVVM uses the DataContextStack to resolve the types of _this, _parent, _root and other expression parameters.

If you have an empty page and use a data-binding, the DataContextStack will be [PageViewModel]. If you add an element and set DataContext={value: ChildObject}, the DataContextStack inside the element will be [PageViewModel, ChildObjectType].

We intend to use the data-binding in the ItemImageUrlBinding property inside the Repeater which changes the binding context to items in the collection set to its DataSource property. Because we don’t know the exact type of the item at the compile time (the collection is just IEnumerable without the exact type specification), we need to be able to calculate the type declaratively using the data context change attributes. That’s why the property specifies two attributes:

  • The ControlPropertyBindingDataContextChange attribute tells DotVVM to use the type of value passed to the specified property (in our case DataSource)
  • The CollectionElementDataContextChange attribute tells DotVVM to take the type (which will be a collection in our case) and calculate the type of its element from it. If the DataSource gets List<string>, the resulting type here will be string. It also supports GridViewDataSet<T> and correctly extracts T from the value.

Now, we can use these properties and pass them to the Repeater control. You can see that we just pass the DataSource property to the Repeater, and set the ItemTemplate to a DelegateTemplate inside which we create instance of another composite control and set its properties.

Since the properties are declared only as method parameters, we cannot use lambda expression or the nameof operator herewe need to specify the property names as string. However, using a composite control from another composite control is still much easier than using the markup control from a code-only control. See how the same thing is done using the traditional approach:

protected override void OnInit(IDotvvmRequestContext context)
{
    base.OnInit(context);

    var title = new HtmlGenericControl("h3");
    title.SetValueRaw(HtmlGenericControl.InnerTextProperty, GetValueRaw(TitleProperty));
    Children.Add(title);

    var repeater = new Repeater();
    repeater.SetValueRaw(ItemsControl.DataSourceProperty, GetValueRaw(DataSourceProperty));
    repeater.ItemTemplate = new DelegateTemplate(serviceProvider =>
        {
            // You cannot do this because ThumbnailPlaceholder is a markup control
            // var control = new ThumbnailPlaceholder();

            // Instead, you need to do this
            var controlBuilderFactory = serviceProvider.GetService<IControlBuilderFactory>();
            var builder = controlBuilderFactory.GetControlBuilder("Controls/Thumbnails/Placeholder/ThumbnailPlaceholder.dotcontrol").builder.Value;
            var control = (ThumbnailPlaceholder)builder.BuildControl(controlBuilderFactory, serviceProvider);

            control.SetBinding(ThumbnailPlaceholder.ImageUrlProperty, GetValueBinding(ItemImageUrlBindingProperty));
            control.Size = 200;

            return control;
        }
    );
    Children.Add(repeater);
}

That’s all for the first part of the article. There will also be a second part which will show how to work with control capabilities and how to avoid inheritance of composite controls which can be a bit problematic.

You can find all the sample code in this GitHub repository.

Tomáš Herceg
Tomáš Herceg

BIO: 

I am the CEO of RIGANTI, small software development company located in Prague, Czech Republic.

I am a Microsoft Regional Director and Microsoft Most Valuable Professional.

I am the author of DotVVM, an open source .NET-based web framework which lets you build Line-of-Business applications easily and without writing thousands lines of Javascript code.

Ostatní články z kategorie: DotVVM Blog