What is coming to DotVVM 5.0: Extensible GridViewDataSet

Published: 5/29/2024 10:56:31 AM

Although DotVVM 4.3 is still in preview, we have already invested a significant effort into several DotVVM 5.0 features. In this blog series, I’ll briefly share some of the features you can look forward to.

The pain of GridViewDataSet

DotVVM declares a handy class called GridViewDataSet, which is basically a List of items displayed in GridView or Repeater, which is accompanied with information about sorting, paging, and the primary key of currently edited row. You can imagine the class properties to be something like this (the actual implementation is a bit more complicated):

public class GridViewDataSet<T>
{
    public IList<T> Items { get; set; }
    public ISortingOptions SortingOptions { get; set; }
    public IPagingOptions PagingOptions { get; set; }
    public IRowEditOptions RowEditOptions { get; set; }    
    ...
}

The interfaces, however, cause several significant problems. The first is polymorphism – when DotVVM deserializes the viewmodel, it does not know the concrete type which will be present in the property. It works because the GridViewDataSet initializes the properties automatically in the constructor and thus, the deserializer does not need to create instances – it just fills the properties of existing objects that are already set in these properties.

Another issue is that the interfaces are not designed well – they are too specific and do not follow the Open-closed principle. For example, the IPagingOptions property contains very opinionated list of properties (such as PageIndex or PageSize), which do not always match the way the paging of data is done (there is no way you could use, for example, continuation tokens that are preferred by many modern APIs).

A similar problem is with the ISortingOptions for it allows sorting based only on a single criterion. Often, you need to sort based on multiple criteria.

Many developers also need to use filtering which is not present in the framework’s GridViewDataSet at all. DotVVM Business Pack has a derived type, BusinessPackDataSet, which adds this functionality, but again, in quite an opinionated and not very extensible way.

New features planned for DotVVM 5.0

We wanted to resolve the issue, but in a way that would impose only minimal or ideally no code changes in the existing applications. Therefore, the functionality of GridViewDataSet will remain unchanged, however, this class will be based on a generic and extensible dataset, and you will be able to compose your own dataset from a primitive building blocks.

Another feature we have already implemented is a support for loading GridViewDataSet using static commands. This has been quite difficult to do, and most applications used the standard command binding and sent the entire viewmodel back to the server to be able to use all of the GridViewDataSet benefits. The GridView and DataPager controls will get the new LoadData property which allows to specify a static command that will be able to load the dataset items based on the new sorting, paging, and filtering options. This will unleash plenty of new possibilities.

And the last feature that will be available thanks to the previously mentioned concepts, is AppendableDataPager. You have for sure met web pages with “infinite” scrolling. Making this experience with classic command binding was a bit unpleasant as the viewmodel size grows with every other chunk of data being loaded. However, thanks to the support of static commands, this can be done more efficiently. The AppendableDataPager has two modes of operation – it can render a custom UI with a button that can trigger loading additional data items, or it can be invisible and load data automatically when you scroll close to its position. Basically, you can just add it below the GridView and once the user scrolls there, new records will be loaded.

GenericGridViewDataSet

In order to make this kind of extensibility, we declared a new class called GenericGridViewDataSet with a bunch of generic arguments. Along with specifying T (the item type), you can specify your custom types for paging, sorting, filtering, row insert and row edit options. There are basic implementations you can use, or you may provide your own ones – you just need to implement the corresponding interfaces (such as IPagingOptions, which have been changed to be empty). Instead, you can compose your options from capability-interfaces, such as IPagingNextPageCapability which tells that this kind of paging supports going to the next page, and so on).

Thanks to this, you can get, for example, token-based paging that is useful when dealing with APIs like GitHub API, which will give you a page of records and provide a link (token) to the next page of records. The only capabilities that are supported is going to the next page, or jumping back to the beginning of the result set. Therefore, the NextTokenPagingOptions implements only the IPagingFirstPageCapability and IPagingNextPageCapability.

The DataPager control was also extended to be more CSS-styleable and configurable, and it automatically follows the capabilities of the dataset that is passed to it. If you use a dataset with NextTokenPagingOptions, only the First and Next buttons will be rendered.

The good old GridViewDataSet<T> class remains as is, however its base class is GenericGridViewDataSet configured to use the concrete paging, sorting, and other options. There can be tiny code changes necessary (for example, the PagingOptions property will not be IPagingOptions but the concrete PagingOptions), but most applications should not be affected by those.

LoadData delegate

As I mentioned previously, GridView and DataPager have a new property called LoadData where you can pass a delegate that will take data set options and return a new data set.

<dot:GridView DataSource="{value: StandardDataSet}"
              LoadData="{staticCommand: myService.LoadData}">
    <Columns>
        <dot:GridViewTextColumn HeaderText="Id" ValueBinding="{value: CustomerId}" AllowSorting="True" />
        <dot:GridViewTextColumn HeaderText="Name" ValueBinding="{value: Name}" AllowSorting="True" />
        <dot:GridViewTextColumn HeaderText="Birth Date" ValueBinding="{value: BirthDate}" FormatString="g" AllowSorting="True" />
        <dot:GridViewTextColumn HeaderText="Message Received" ValueBinding="{value: MessageReceived}" AllowSorting="True" />
    </Columns>
</dot:GridView>
<dot:DataPager DataSet="{value: StandardDataSet}" 
               LoadData="{staticCommand: myService.LoadData}"/>

The LoadData method can look like this:

[AllowStaticCommand]
public async Task<NextTokenGridViewDataSet> LoadToken(
    GridViewDataSetOptions<NoFilteringOptions, SortingOptions, CustomerDataNextTokenPagingOptions> options
)
{
    var dataSet = new NextTokenGridViewDataSet();
    dataSet.ApplyOptions(options);
    await dataSet.LoadFromQueryableAsync(dbContext.Customers);
    return dataSet;
}

The NextTokenGridViewDataSet class was declared like this:

public class NextTokenGridViewDataSet 
    : GenericGridViewDataSet<CustomerData, NoFilteringOptions, SortingOptions, CustomerDataNextTokenPagingOptions, RowInsertOptions<CustomerData>, RowEditOptions>
{
    public NextTokenGridViewDataSet() : base(
        new NoFilteringOptions(), 
        new SortingOptions(), 
        new CustomerDataNextTokenPagingOptions(), 
        new RowInsertOptions<CustomerData>(), 
        new RowEditOptions())
    {
    }
}
        
public class CustomerDataNextTokenPagingOptions : NextTokenPagingOptions, IApplyToQueryable, IPagingOptionsLoadingPostProcessor
{
    public IQueryable<T> ApplyToQueryable<T>(IQueryable<T> queryable)
    {
        var token = int.Parse(CurrentToken ?? "0");

        return queryable.Cast<CustomerData>()
            .OrderBy(c => c.CustomerId)
            .Where(c => c.CustomerId > token)
            .Take(3)
            .Cast<T>();
    }

    public void ProcessLoadedItems<T>(IQueryable<T> filteredQueryable, IList<T> items)
    {
        var lastToken = items.Cast<CustomerData>()
            .OrderByDescending(c => c.CustomerId)
            .FirstOrDefault()?.CustomerId;

        lastToken ??= 0;
        if (lastToken == 12)
        {
            NextPageToken = null;
        }
        else
        {
            NextPageToken = lastToken.ToString();
        }
    }
}

As you can see, I have declared a custom type of GridViewDataSet with custom CustomerDataNextTokenPagingOptions. Declaring custom paging options is optional, however I wanted use LoadFromQueryable – therefore, I implemented IApplyToQuerable and IPagingOptionsLoadingPostProcessor that will handle all the ceremony with tokens. In real-world case, this would probably be implemented in a different way, as I would use this kind of paging for some API.

AppendableDataPager

The last bit of interest is the new paging component that supports the “infinite” scrolling. As you can see, it has several templates:

  • LoadTemplate specifies how the “Load more items” button would look like. If it is not specified, the pager will be invisible and will trigger automatically when being scrolled to.
  • LoadingTemplate specifies what the pager renders while the data is being loaded. You can easily use some kind of skeleton loading animation.
  • EndTemplate specifies what will be rendered when you reach at the end of the data (meaning that there are no more items).
<dot:AppendableDataPager DataSet="{value: Customers}"
                         LoadData="{staticCommand: RootViewModel.LoadNextPage}">
    <LoadTemplate>
        <dot:Button Text="Load more" Click="{staticCommand: _dataPager.Load()}" />
    </LoadTemplate>
    <LoadingTemplate>
        <span class="loading">Your data are on the way...</span>
    </LoadingTemplate>
    <EndTemplate>
        <span class="loaded">You reached to the end of the Earth. Now you shall see the  and .</span>
    </EndTemplate>
</dot:AppendableDataPager>


We need feedback

We have not released a preview version of DotVVM yet, but you can grab the source code and use it in your project as Git submodule. The new datasets are in the feature/new-datasets branch, so feel free to try them and let us know what you think!

Currently, we are still working on integrating these changes to Bootstrap and Business Pack packages – if you use them in your project, it will probably not work well. However, you can at least try to run the sample project and examine the GridViewStaticCommand or AppendableDataPager control samples.

Tomáš Herceg

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

I am Microsoft Most Valuable Professional and the founder of DotVVM project.