Search code examples
c#.net-coredotvvm

Rendering an ITemplate in custom markup control in DotVVM


I'm trying to create a custom control that renders a calendar in which days can be selected. I need the ability to pass an ITemplate to the control which will be rendered for each day. The days are rendered by a Repeater.

As there is currently no existing control that allows binding to an ITemplate (or use an ITemplate with anything other than a collection), how do I render an ITemplate from a controlProperty easily? I'd prefer to have some kind of control that just renders an ITemplate so it can be reused elsewhere.

Partial control markup:

    <!-- ... -->
    <dot:Repeater ID="DaysRepeater" DataSource="{value: Days}" class="list-group list-group-flush calendar-grid">
        <ItemTemplate>
            <div class="{{value: "calendar-day calendar-day-" + DayOfWeekIndex }}">
                <dot:LinkButton ID="DayButton" class="{{value: "list-group-item list-group-item-action " + (Selected ? "active calendar-day-btn" : "calendar-day-btn") }}"
                                Click="{controlCommand: SelectDate(_this.Date)}">
                    {{value: DayText}}
                    <!-- RENDER TEMPLATE HERE -->
                </dot:LinkButton>
            </div>
        </ItemTemplate>
    </dot:Repeater>

ItemTemplate in code-behind:

        [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement, Required = false)]
        [ConstantDataContextChange(typeof(ICollection<CalendarDayModel>)), CollectionElementDataContextChange(1)]
        public ITemplate ItemTemplate
        {
            get { return (ITemplate)GetValue(ItemTemplateProperty)!; }
            set { SetValue(ItemTemplateProperty, value); }
        }

        public static readonly DotvvmProperty ItemTemplateProperty =
            DotvvmProperty.Register<ITemplate, Calendar>(t => t.ItemTemplate);

Example usage of control:

<cc:Calendar DataContext="{value: CalendarViewModel}" MultiSelect="true">
    <ItemTemplate>
        Selected: {{value: Selected}}
    </ItemTemplate>
</cc:Calendar>

Solution

  • Unfortunately, we don't have any control in DotVVM that could bind a template and just render it. However, you can do the following trick:

    1. Use the PlaceHolder control in the template of the Repeater:
    <dot:Repeater ID="DaysRepeater" DataSource="{value: Days}" class="list-group list-group-flush calendar-grid">
        <ItemTemplate>
            <div class="{{value: "calendar-day calendar-day-" + DayOfWeekIndex}}">
                <dot:LinkButton ID="DayButton" class="{{value: "list-group-item list-group-item-action " + (Selected ? "active calendar-day-btn" : "calendar-day-btn") }}"
                                Click="{controlCommand: SelectDate(_this.Date)}">
                    {{value: DayText}}
                    <dot:PlaceHolder ID="TemplateHost" />
                </dot:LinkButton>
            </div>
        </ItemTemplate>
    </dot:Repeater>
    
    1. Replace the Repeater template with your own one that will first render the original template, then find the placeholder, and puts the inner template inside:
    protected override void OnInit(IDotvvmRequestContext context)
    {
        var repeater = (Repeater)FindControlInContainer("DaysRepeater");
        repeater!.ItemTemplate = new TemplateWrapper(ItemTemplate, repeater!.ItemTemplate);
    
        base.OnInit(context);
    }
    
    class TemplateWrapper : ITemplate 
    {
        private readonly ITemplate innerTemplate;
        private readonly ITemplate repeaterTemplate;
    
        public TemplateWrapper(ITemplate innerTemplate, ITemplate repeaterTemplate)
        {
            this.innerTemplate = innerTemplate;
            this.repeaterTemplate = repeaterTemplate;
        }
    
        public void BuildContent(IDotvvmRequestContext context, DotvvmControl container)
        {
            repeaterTemplate.BuildContent(context, container);
    
            var placeholder = container.FindControlInContainer("TemplateHost");
            innerTemplate.BuildContent(context, placeholder!);
        }
    }