Guidance Creating new Composite Control

I am trying to create a composite control consisting of the Business Pack DataPager and some other controls. The end goal is to have a single component similar to the following:

The BP DataPager will be the primary control in the new composite control. The new control needs a DataSet property for use by the DataPager and that DataSet needs to be the same object as the associated GridView’s DataSource property. (This would be the same as when you use the Business Pack GridView and the BP DataPager together.)

I have tried to piece together what to do from the instructions on the following three blog posts, but they do not seem to cover the more advanced (or maybe it’s basic) scenario I am facing:

No matter what I try, I cannot get the new control to work.

Here are two of the versions I have tried, neither of which work correctly:
Version 1

    [ControlMarkupOptions(AllowContent = false)]
    public class DataPagerAsCompositeControlV1 : CompositeControl
    {
        public static DotvvmControl GetContents
        (
            [MarkupOptions(Required = true)]
                ValueOrBinding<IBusinessPackDataSet> dataSet
        )
        {
            var bindingCompilationService = new BindingCompilationService(
                new OptionsManager<BindingCompilationOptions>(new OptionsFactory<BindingCompilationOptions>(
                    new List<IConfigureOptions<BindingCompilationOptions>>(),
                    new List<IPostConfigureOptions<BindingCompilationOptions>>())),
                new DefaultExpressionToDelegateCompiler(DotvvmConfiguration.CreateDefault()),
                new DefaultDotvvmCacheAdapter());

            return new HtmlGenericControl("div")
                .AppendChildren
                    (
                        new DotVVM.BusinessPack.Controls.DataPager(new DotVVM.Framework.Controls.DataPager.CommonBindings(bindingCompilationService), bindingCompilationService)
                            .SetProperty(d => d.DataSet, dataSet.BindingOrDefault)
                    );
        }
    }

Version 2

    [ControlMarkupOptions(AllowContent = false)]
    public class DataPagerAsCompositeControlV2 : CompositeControl
    {
        public static IEnumerable<DotvvmControl> GetContents
        (
            //[MarkupOptions(Required = true)]
            [ControlPropertyBindingDataContextChange("DataSet", order: 0), CollectionElementDataContextChange(order: 1)]
                IValueBinding<IBusinessPackDataSet> dataSet
        )
        {
            yield return new HtmlGenericControl("div")
                .SetProperty("id", "testingTheThing");


            var bindingCompilationService = new BindingCompilationService(
                new OptionsManager<BindingCompilationOptions>(new OptionsFactory<BindingCompilationOptions>(
                    new List<IConfigureOptions<BindingCompilationOptions>>(),
                    new List<IPostConfigureOptions<BindingCompilationOptions>>())),
                new DefaultExpressionToDelegateCompiler(DotvvmConfiguration.CreateDefault()),
                new DefaultDotvvmCacheAdapter());

            yield return new DotVVM.BusinessPack.Controls.DataPager(
                new DotVVM.Framework.Controls.DataPager.CommonBindings(bindingCompilationService),
                bindingCompilationService)
                .SetProperty(p => p.DataSet, dataSet);
        }
    }

When version 1 is on the page, I get a DataPager to show up correctly. However, clicking on any of the links within it causes the following error to occur:

DotVVM.Framework.Runtime.Commands.InvalidCommandInvocationException

Nothing was found inside specified DataContext. Please check if ViewModel is populated.

Interestingly, if I add a plain BP DataPager to the page and set its DataSet property to the same object as my control and the GridView, then all the buttons work on my control.

When version 2 is on the page I get the following error as soon as the page loads, with or without the BP DataPager:

DotVVM.Framework.Compilation.DotvvmCompilationExceptionCould not compute the type of DataContext: System.Exception: Property ‘DataSet’ is required on ‘DataPagerAsCompositeControlV2’.
at DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute.GetChildDataContextType(ITypeDescriptor dataContext, IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor property)
at DotVVM.Framework.Compilation.ControlTree.ControlTreeResolverBase.ApplyContextChange(IDataContextStack dataContext, DataContextChangeAttribute attributes, IAbstractControl control, IPropertyDescriptor property)
at DotVVM.Framework.Compilation.ControlTree.ControlTreeResolverBase.GetDataContextChange(IDataContextStack dataContext, IAbstractControl control, IPropertyDescriptor property)

I do not know how to proceed. I have not been able to find any other more clear instructions on how to create a composite control and pass the properties the inner controls need to use.

Please help.

Thank you!

Here is the usage of the controls:

<testing:DataPagerAsCompositeControlV1 DataSet="{value: CacheData}" />

And here is the CacheData property in the page view model:

public BusinessPackDataSet<ResourceCacheKeyDM> CacheData { get; set; } = new BusinessPackDataSet<ResourceCacheKeyDM>()
{
    PagingOptions = {
        PageSize = 10
    }
};

ResourceCacheKeyDM is a simple class defined as follows:

public class ResourceCacheKeyDM
{
    public int Id { get; set; }
    public string Type { get; set; }
    public string KeyName { get; set; }
    public string KeyValue { get; set; }
}

Thanks for reaching out, sorry, our documentation on the non-trivial use cases is still quite incomplete.

First of all, you can save yourself quite a bit of pain by making the GetContents method instance (non-static) and asking for DataPager.CommonBindings and BindingCompilationServicein the constructor. Something like the following (I’m writing from memory):

[ControlMarkupOptions(AllowContent = false)]
public class DataPagerAsCompositeControlV1 : CompositeControl
{
    public DataPagerAsCompositeControlV1(BindingCompilationService bindingService, DotVVM.Framework.Controls.DataPager.CommonBindings dataPagerCommonBindings)
    {
        this.bindingService = bindingService;
        this.dataPagerCommonBindings = dataPagerCommonBindings;
    }
    public DotvvmControl GetContents
    (
        [MarkupOptions(Required = true)]
        IValueBinding<IBusinessPackDataSet> dataSet
    )
    {
        return new HtmlGenericControl("div")
            .AppendChildren(
                    new DotVVM.BusinessPack.Controls.DataPager(dataPagerCommonBindings, bindingService)
                        .SetProperty(d => d.DataSet, dataSet)
                );
    }
}

The static GetContents is generally preferred, until it gets in the way too much. If it’s static, it just makes it easier to reuse the logic from other parts of your codebase.

You don’t need the ...DataContextChange attributes for this property. The attributes instruct the compiler which data context type should be inside this property. The ControlPropertyBindingDataContextChange gets a result type of another property on this control, CollectionElementDataContextChange then unwraps IEnumerable<T> into T. In this case it’s crashing because the data context of DataSet depends on the result type of DataSet which is still not calculated. I think you won’t need these attributes for control at all, you’d use it if you wanted to add a binding (or ITemplate) evaluated for each item in the DataSet.

As for the InvalidCommandInvocationException error, I don’t know why exactly is occurring. Could you please share the entire error screen with me? There should be more details. Could you also try using the built-in DataPager instead of the one from BusinessPack? It is bit simpler, so we could reduce the number of places which might be buggy.

1 Like

Thank you @exyi This is helpful. I am out of the office the next couple days. I will work on this again on Monday and let you know how it goes.

Thank you, again, @exyi. Your advice helped a lot. Your suggestions got my control to work. Below is the code as of now before I add other controls to it. Currently is just the BP Datapager wrapped in a div but it is functioning with a BP GridView.

        private readonly BindingCompilationService _bindingCompilationService;
        private readonly DataPager.CommonBindings _dataPagerCommonBindings;

        public DataPagerAsCompositeControlV1(BindingCompilationService bindingCompilationService, DotVVM.Framework.Controls.DataPager.CommonBindings dataPagerCommonBindings)
        {
            _bindingCompilationService = bindingCompilationService;
            _dataPagerCommonBindings = dataPagerCommonBindings;
        }

        public DotvvmControl GetContents
        (
            [MarkupOptions(Required = true)]
                ValueOrBinding<IBusinessPackDataSet> dataSet
        )
        {
            return new HtmlGenericControl("div")
                .AppendChildren
                    (
                        new BPControls.DataPager(_dataPagerCommonBindings, _bindingCompilationService)
                            .SetProperty(d => d.DataSet, dataSet.BindingOrDefault)
                    );
        }