Building controls with parameterized binding expressions in DotVVM

|
Publikováno:

Recently, we got a question on our Gitter chat about building markup controls that use binding expressions.

Basically, the requirement was to have a breadcrumb navigation control that would be used this way:

<cc:Breadcrumb DataSource="{value: ListNavParent}" 
               ItemActiveBinding="{value: ItemActive}" 
               PublicUrlBinding="{value: PublicUrl}" 
               TitleBinding="{value: Title}" />

The first parameter is DataSource - a hierarchy of pages in the navigation (Section 1 > Subsection A > Page C). The remaining parameters are binding expressions that would be evaluated on every item in the data source and provide the URL and title of the item and the information whether the item is active.

DotVVM has several controls which work this way – ComboBox for example. There is also the DataSource property as well as ItemTextBinding, ItemValueBinding and so on.

However, it is not trivial to build a control like that. In this article, I’d like to suggest some ways how to do it.


Rule 1: Use DataContext wherever possible

Writing such control can be much simpler if you don’t try to deal with properties like DataSource and SomethingBinding. Since the control just renders the breadcrumb navigation (a bunch of links) and does nothing else, and the collection of links will probably be stored in the viewmodel anyway (you need to pass it in the DataSource property somehow), you can get rid of the DataSource property and pass the collection of links to the control via the DataContext property. Inside the control, all bindings will be evaluated just on the control’s DataContext.

And because the DataContext will be a collection of some objects of a known type, making the Breadcrumb control would be easy. If you want to use the control in a more generic way (for different kinds of objects), feel free to use and interface.

First, let’s create the object which would represent the individual link in the navigation:

public class BreadcrumbLink
{
    public string Title { get; set; }

    public string Url { get; set; }

    public bool IsActive { get; set; }
}


It would be nice if we could display also a home icon in the control – the control will need a URL for the home icon as well as the collection of links.

If you have multiple things you need to pass to the control, there are two options:

  • Pass one of the things (probably the main one) as the DataContext and create control properties for the other values.
  • <cc:Breadcrumb DataContext="{value: Links}" HomeUrl="/" />


  • Create a tiny viewmodel object that holds all information the control needs. It depends how “coupled” the values are to each other – if you need to aggregate various data from different places in the viewmodel, the first option would work better. If all the values are tied together and worked with from one place, this approach is better.

Let’s create a tiny viewmodel object for the entire control:

public class BreadcrumbViewModel
{

    public string HomeUrl { get; set; }

    public List<BreadcrumbLink> Links { get; set; }

}


Now we can define the control like this:

@viewModel RepeaterBindingSample.Controls.BreadcrumbViewModel, RepeaterBindingSample

<ol class="breadcrumb">
    <li><a href="{value: HomeUrl}"><span class="glyphicon glyphicon-home" /></a></li>
    <dot:Repeater DataSource="{value: Links}" RenderWrapperTag="false">
        <ItemTemplate>
            <li class-active="{value: IsActive}">
                <a href="{value: Url}">{{value: Title}}</a>
            </li>
        </ItemTemplate>
    </dot:Repeater>
</ol>


In this case, the control doesn’t need to have a code-behind file as it won’t need any control properties. All we need to do is to prepare the BreadcrumbViewModel object in the page viewmodel (nesting of viewmodels is a very powerful thing btw) and pass it to the control. The @viewModel directive of the control enforces that the control can be used only when its DataContext is BreadcrumbViewModel. If you use the control on any other place, you’ll get an error – bindings in DotVVM are strongly-typed.

<cc:Breadcrumb DataContext="{value: Breadcrumb}" />

Since this control will probably be on all pages of the app, it can be used in the master page. In addition to that, the master page can require all pages to provide items for the breadcrumb navigation using an abstract method:

public abstract class MasterPageViewModel : DotvvmViewModelBase
{

    [Bind(Direction.ServerToClientFirstRequest)]
    public BreadcrumbViewModel Breadcrumb { get; } = new BreadcrumbViewModel()
    {
        HomeUrl = "/"
    };

    public override Task Init()
    {
        if (!Context.IsPostBack)
        {
            Breadcrumb.Links = new List<BreadcrumbLink>(ProvideBreadcrumbLinks());
        }

        return base.Init();
    }

    protected abstract IEnumerable<BreadcrumbLink> ProvideBreadcrumbLinks();
}


The conclusion is – pass data to controls using DataContext and avoid creating code behind for the controls if you can.


But what if I really want to work with custom binding expressions?

Building of custom binding expressions is tricky, but very powerful. Before we start, let’s review how binding works in DotVVM.

The DotVVM controls define their properties using this awful boilerplate syntax:

public string HomeUrl
{
    get { return (string)GetValue(HomeUrlProperty); }
    set { SetValue(HomeUrlProperty, value); }
}
public static readonly DotvvmProperty HomeUrlProperty
    = DotvvmProperty.Register<string, Breadcrumb2>(c => c.HomeUrl, string.Empty);


Why is that? Because the property can contain either its value, or a data-binding expression (which is basically an object implementing the IBinding interface). DotVVM controls have a dictionary of property values-or-bindings, and offer the GetValue and SetValue methods that can work with the property values (including evaluation of the binding expression). The only important thing here is the static readonly field HomeUrlProperty which is a DotVVM property descriptor. It can specify the default value, it supports inheritance from parent controls (if you set DataContext property on some control, its value will propagate to the child controls - unless the child control sets DataContext to something else), and there are more features to that.

There are several types of bindings – the most frequent is the value and command bindings. But how to construct the binding object?

The easiest way is to have it constructed by the DotVVM – it means specify it somewhere in a DotHTML file and have it compiled by the DotVVM view engine. It happens when you declare the code-behind for your markup control:

@viewModel System.Object
@baseType RepeaterBindingSample.Controls.Breadcrumb2, RepeaterBindingSample


public class Breadcrumb2 : DotvvmMarkupControl
{

    public string HomeUrl
    {
        get { return (string)GetValue(HomeUrlProperty); }
        set { SetValue(HomeUrlProperty, value); }
    }
    public static readonly DotvvmProperty HomeUrlProperty
        = DotvvmProperty.Register<string, Breadcrumb2>(c => c.HomeUrl, string.Empty);

}

If you use the control on some page and look what’s in the HomeUrl property (e. g. using GetValueRaw(HomeUrlProperty)), you’ll get the constructed binding object.

There are several things inside the binding object – the bare minimum are these:

  • DataContextStack – this structure represents the hierarchy of types in the DataContext path – if the binding is in the root scope of the page, the stack has only PageViewModel; if it’s nested in a repeater, there will be PageViewModel and BreadcrumbLink types. Please note that these are only types, not the actual values. The binding must know of that type is _this, _parent and  _root.
  • LINQ expression that represents the binding itself. It is strongly-typed, it can be compiled to IL so it’s fast to evaluate, and it can be quite easily translated to JS because it’s an expression tree. The expression is basically a lambda with one parameter of object[] – this is a stack of DataContexts (not types but the actual values – they are passed to the binding so it can evaluate). The element 0 is _this, the element 1 is _parent, the last one is _root

There can be other things, like translated Knockout expression of the binding, original binding string and so on, but they are either optional or will be generated automatically when needed. If you want to construct your own binding, you need at least the DataContextStack and the expression.

To create a binding, you can use something like this:

In order to build a binding, you need an instance of BindingCompilationService, the expression and the DataContextStack.

You can obtain the BindingCompilationService from dependency injection – just request it in the control constructor.

The DataContextStack can be obtained by calling control.GetDataContextType().

An finally, you need the expression – you can define it yourself like this:

Expression<Func<object[], string>> expr = contexts => ((BreadcrumbViewModel)contexts[0]).HomeUrl;

Note that the expression is always Func<object[], ResultType>. The contexts variable is the array of data context – the first element (0) is _this. The code snippet above is equivalent to {value: _this.HomeUrl} or just {value: HomeUrl}. The cast to specific type is needed just because contexts is array of objects.

To build the expression and pass it for instance to the Literal control, you need something like this:

// build the binding for literal yourself
Expression<Func<object[], string>> expr = contexts => ((BreadcrumbViewModel)contexts[0]).HomeUrl;
var dataContextStack = this.GetDataContextType();
var binding = ValueBindingExpression.CreateBinding(bindingCompilationService, expr, dataContextStack);

// create the literal control and add it as a child
var literal = new Literal();
literal.SetBinding(Literal.TextProperty, binding);
Children.Add(literal);

So now you know how to work with bindings – if you’d like to create a control with a property that accepts a binding, or if you need to compose your own binding, that’s how this is done.


Now how would I create a control as described at the beginning?


If you really want to create a control which is used in the following way, there is some extra things that need to be done.

<cc:Breadcrumb DataSource="{value: ListNavParent}" ItemActiveBinding="{value: ItemActive}" PublicUrlBinding="{value: PublicUrl}" TitleBinding="{value: Title}" />

The DataSource property contains a binding that is evaulated in the same DataContextStack in which the control lives. If you use this control in the root scope of the page, its DataContext will be just PageViewModel.

But the remaining properties will actually be evaluated on the individual items in the DataSource collection. They need to have the DataContextStack equal to the (PageViewModel, BreadcrumbLink) pair. Also, the IntelliSense in Visual Studio will need to know that so it could offer you the property names.

That’s why these properties have to be decorated with DataContextChange attributes. These attributes can stack on top of each other and basically they tell how to change the DataContextStack in order to get the result.

The TitleBinding property will be declared like this:

[ControlPropertyTypeDataContextChange(order: 0, propertyName: nameof(DataSource))]
[CollectionElementDataContextChange(order: 1)]
public IValueBinding<string> TitleBinding
{
    get { return (IValueBinding<string>)GetValue(TitleBindingProperty); }
    set { SetValue(TitleBindingProperty, value); }
}
public static readonly DotvvmProperty TitleBindingProperty
    = DotvvmProperty.Register<IValueBinding<string>, Breadcrumb2>(nameof(TitleBinding));


The first attribute says “let’s take the type of the value assigned to the DataSource property”. The second says “let’s take that type; it should be a collection or an array, so let’s take the element type of this and use it as a second level in the DataContextStack”.

So, if you assign a collection of type List<BreadrumbLink> in the DataSource property, the TitleBinding property stack will be (PageViewModel, BreadcrumbLink).

Notice that the type of the property is IValueBinding<string> – this hints everyone that they shouldn’t fin the “actual value” in the property.

Also, if you use the binding generated in this property, make sure you use it in the correct DataContextStack.

How to use binding object in DotHTML?

Unfortunately, right now you can’t. I think it is possible to write an attached property that would do that (I will try it), but DotVVM doesn’t ship with any option to do that. 

However, it is possible to generate the controls from the C# code. Theoretically, you can combine markup controls and code-only approach of tampering with bindings, but sometimes it gets even more complicated, so I prefer using code-only control in these cases.

Let’s start by defining the Breadcrumb2 control and its properties:

public class Breadcrumb2 : ItemsControl 
{

    public string HomeUrl
    {
        get { return (string)GetValue(HomeUrlProperty); }
        set { SetValue(HomeUrlProperty, value); }
    }
    public static readonly DotvvmProperty HomeUrlProperty
        = DotvvmProperty.Register<string, Breadcrumb2>(c => c.HomeUrl, string.Empty);
    
    [ControlPropertyTypeDataContextChange(order: 0, propertyName: nameof(DataSource))]
    [CollectionElementDataContextChange(order: 1)]
    public IValueBinding ItemActiveBinding
    {
        get { return (IValueBinding)GetValue(ItemActiveBindingProperty); }
        set { SetValue(ItemActiveBindingProperty, value); }
    }
    public static readonly DotvvmProperty ItemActiveBindingProperty
        = DotvvmProperty.Register<IValueBinding, Breadcrumb2>(nameof(ItemActiveBinding));

    [ControlPropertyTypeDataContextChange(order: 0, propertyName: nameof(DataSource))]
    [CollectionElementDataContextChange(order: 1)]
    public IValueBinding PublicUrlBinding
    {
        get { return (IValueBinding)GetValue(PublicUrlBindingProperty); }
        set { SetValue(PublicUrlBindingProperty, value); }
    }
    public static readonly DotvvmProperty PublicUrlBindingProperty
        = DotvvmProperty.Register<IValueBinding, Breadcrumb2>(nameof(PublicUrlBinding));

    [ControlPropertyTypeDataContextChange(order: 0, propertyName: nameof(DataSource))]
    [CollectionElementDataContextChange(order: 1)]
    public IValueBinding TitleBinding
    {
        get { return (IValueBinding)GetValue(TitleBindingProperty); }
        set { SetValue(TitleBindingProperty, value); }
    }
    public static readonly DotvvmProperty TitleBindingProperty
        = DotvvmProperty.Register<IValueBinding, Breadcrumb2>(nameof(TitleBinding));


}


The class inherits from ItemsControl, a handy base class that is common for all controls which have the DataSource property. Building your own DataSource property is not that easy if you want it to support all the nice things like the _index variable – you can look in the ItemsControl source code how it’s done.

Otherwise, the code snippet above is not complicated – it is just long. It declares the HomeUrl property as well as the ItemActiveBinding, PublicUrlBinding and TitleBinding with the DataContextChange attributes.

Now, let’s build the contents of the control. Since it doesn’t depend on the actual data passed to the control, we can do this in the Init phase – in general, the earlier the better.

protected override void OnInit(IDotvvmRequestContext context)
{
    var ol = new HtmlGenericControl("ol");
    ol.Attributes["class"] = "breadcrumb";
    ol.Children.Add(BuildHomeItem());
    ol.Children.Add(BuildRepeater());
    Children.Add(ol);

    base.OnInit(context);
}


The BuildHomeItem method just builds the first piece of markup:

<li><a href="{value: HomeUrl}"><span class="glyphicon glyphicon-home" /></a></li>
private HtmlGenericControl BuildHomeItem()
{
    var homeLi = new HtmlGenericControl("li");
    {
        var homeLink = new HtmlGenericControl("a");
        {
            homeLink.Attributes["href"] = GetValueRaw(HomeUrlProperty);     // hard-coded value or binding

            var homeSpan = new HtmlGenericControl("span");
            {
                homeSpan.Attributes["class"] = "glyphicon glyphicon-home";
            }
            homeLink.Children.Add(homeSpan);
        }
        homeLi.Children.Add(homeLink);
    }
    return homeLi;
}


Btw, I am used to nest the child control creation in the inner blocks – it helps me to orient in large hierarchies of the controls.


Creation of the Repeater involves implementation of the ItemTemplate class:


<dot:Repeater DataSource="{value: Links}" RenderWrapperTag="false">
    <ItemTemplate>
        <li class-active="{value: IsActive}">
            <a href="{value: Url}">{{value: Title}}</a>
        </li>
    </ItemTemplate>
</dot:Repeater>


private Repeater BuildRepeater()
{
    var repeater = new Repeater();
    repeater.RenderWrapperTag = false;
    repeater.ItemTemplate = new BreadcrumbLinkTemplate(this);
    repeater.SetBinding(Repeater.DataSourceProperty, GetValueBinding(DataSourceProperty));

    return repeater;
}


internal class BreadcrumbLinkTemplate : ITemplate
{
    private Breadcrumb2 parent;

    public BreadcrumbLinkTemplate(Breadcrumb2 parent)
    {
        this.parent = parent;
    }

    public void BuildContent(IDotvvmRequestContext context, DotvvmControl container)
    {
        var li = new HtmlGenericControl("li");
        {
            li.CssClasses.AddBinding("active", parent.GetValueBinding(Breadcrumb2.ItemActiveBindingProperty));

            var link = new HtmlGenericControl("a");
            {
                link.Attributes["href"] = parent.GetValueBinding(Breadcrumb2.PublicUrlBindingProperty);
                link.SetBinding(HtmlGenericControl.InnerTextProperty, parent.GetValueBinding(Breadcrumb2.TitleBindingProperty));
            }
            li.Children.Add(link);
        }
        container.Children.Add(li);
    }
}


And we’re done – you can find the complete code on GitHub.


As you can see, building the control the second way is much more complicated. Partly, it’s because you are trying to build a very universal control that is agnostic to the type of items passed to the DataSource collection, and accepts binding expressions as parameters. Thanks to the strongly-typed nature of DotVVM, you need to deal with bindings, data context stacks and changes, and all these things you wouldn’t have in the dynamic world. And finally, we didn’t have much time to make this simple – we focused on other areas than simplifying control development, although it is an important one too.

We had some ideas and we even have an experimental implementation, but we didn’t have the time to finalize the pull request and make it a real framework feature.

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: Tomáš Herceg Blog