dotvvm icon indicating copy to clipboard operation
dotvvm copied to clipboard

Better data controls - Design

Open Mylan719 opened this issue 3 years ago • 11 comments

Motivation

Recently we have grown to like staticCommands in our DotVVM applications. They are fast, they save on network traffic and they are easy for our UX team to modify.

Grids, Pagers, and Repeaters have served us very well, however, they were not designed with static commands in mind. That makes using them in static-command-only applications impossible. The repeater works fine. It does not care as long as the DataSource property is set. GridView and Pager on the other hand have ordinary commands baked into them. As soon as you use GridView sorting or go to the next page using the current pager, you are forced to do a full postback.

In this design document, we present changes to enhance and modernize GridView, Repeater, Pager, and GridViewDataSet with static commands in mind.

Example of proposed features in use:

<body>
<DataControls.TemplateProvider>
   <dot:CustomTemplateProvider>
      <PagerFirstTemplate>
           <!-- ... //-->
      </PagerFirstTemplate>
      <!-- ... //-->
      <GridColumnHeaderTemplate>
         <span>
            <cc:Icon Type="{value: _column.Name}">
            <dot:LinkButton Click={staticCommand: Sorter.Sort(_column.Name)} Text={value: _column.Name}/>
         </span>
      </GridColumnHeaderTemplate>
   </dot:CustomTemplateProvider>
</DataControls.TemplateProvider>

<!-- ... //-->
<form>
    <dot:TextBox Value={value: SearchText} />
    <dot:Button Click={staticCommand:  Customers.Pager.Page(0); Customers.RequestRefresh()} Text="Search"/>
</form>
<dot:DataSetLoader DataSource={value: Customers} Load={staticComand: options => service.CustomersGrid(options, SearchText)} />
<dot:GridView DataSource={value: Customers} >
     <!-- ... //-->
</dot:GridView>
<dot:Pager DataSource={value: Customers} />

</body>

DataSet design

  • GridViewDataSet has the Items property, same as before
  • Pager, Sorter, Filter properties are moved to separate DTO under the Options property. This makes passing the options as an argument easier. For example when calling a service method as a static command. There is little reason to send grid items back to a server.
  • RequestRefresh() method on the data set has its javascript translator registered so it works when called in a static command. When called from a static command it updates the data set options in javascript, then finds a loader callback and reloads the items from the server.
  • Pager object has methods like First(), Previous(), Page(int index), Next(), Last(). These methods have their default implementation in C# that is used if called from standard command. However, those methods also have their javascript translations registered for use from a static command.
  • Sorter has the method Sort(...) that also has the C# equivalent for commands and javascript translation for static commands.

The new DataSetLoader control

The reason for this control is to decouple data source loading from the controls like grids, pagers, and other controls meant to display the data.

  • Property DataSource binds a loader to a data source. Multiple grids and repeaters can display the data source, but for each data source, there is only one loader.
  • Command Property Load static command containing a lambda function that returns filled GridViewDataSet. Example: (DataSetOptions opt) => myService.MyLoad(opt,...).

How it works

On client javascript code, the loader is connected with a grid, repeater, pager, or any other items control by using the same object in their DataSource property.

When GridViewDataSet.RequestRefresh() is called from a static command the DataSetLoader for the data set is located. Then lambda function from property DataSetLoader.Load is called and the data set is updated with the new data returned from the server. This is part of javascript translation for the GridViewDataSet.RequestRefresh method.

DataControls.TemplateProvider Extension property

In the past, it has been very hard to customize DotVVM Pager and GridView controls. For this reason, we created DataControls.TemplateProvider. The property is inherited from parent elements. It can be set for instance in a master page on body and it will affect all grids and pagers in the application.

The property contains controls derived from DataControlsTemplateProvider. We created default Template providers to choose from:

  • CommandTemplateProvider - is the default. All the interactions like sorting and paging are handled by ordinary command bindings and will result in postbacks.
  • StaticCommandTemplateProvider - All the interactions like sorting and paging are handled by stratic command bindings. Here, javascript translations of methods on GridViewDataSet get called.
  • CustomTemplateProvider - This provider contains template properties that allow the user to specify the markup themselves:
    • PagerFirstTemplate - Pager first button content
    • PagerNextTemplate - Pager next button template
    • PagerPageNumberTemplate - Pager page item button template
    • PagerPreviousTemplate - Pager previous button template
    • PagerLastTemplate - Pager last button template
    • ColumnHeaderTemplate - GridView column header template

Mylan719 avatar Apr 25 '21 17:04 Mylan719

@Mylan719 I think that you have a mistake there next and forwad?

martindybal avatar Apr 25 '21 17:04 martindybal

What's signature of Sorter.Sort? Can I use it for multi criteria sort?

martindybal avatar Apr 25 '21 17:04 martindybal

The signature of Sorter is not defined yet. There are several possibilities.

  1. ICommandBinding Sorter.Sort(string columnName)
  2. ICommandBinding Sorter.Sort(int index)
  3. ICommandBinding Sorter.Sort(string columnName, SortingDirection direction) etc..

The question is what the templates should allow being set. Also, this API should comply with the possibility of implementing filters.

quigamdev avatar Apr 25 '21 19:04 quigamdev

I'm not sure about the fact that <DataControls.TemplateProvider> is a property that allows you to set only one template provider. In a moment when TemplateProvider has Type property, DataControls.TemplateProvider could be an array and a provider for Pager and GridView could be split into two providers. I'm aware that it does not make much sense to split the provider for pager and gridview. What I am aiming for is a generic method that could potentially replace server-side styles.

quigamdev avatar Apr 25 '21 19:04 quigamdev

@martindybal Since most users want just single-criterion sorting, I'd prefer keeping SortingOptions as they are now. However, it will be possible to plug in your own MultiCriteriaSortingOptions which can do that.

From the control's perspective, I think that the Sort method should only take the sortExpression parameter (no matter what concrete sorting options you plan to use). Even if you use multi-criteria sorting, the user will click the GridView column header, and the sorting optionsmust deal with that - e. g. reverse the sort order, add the column as a second criterion etc. Also, everyone will be able to set properties on the concrete sorting options, so if you want to define custom UI to define the sort criteria, you'll be able to do it.

tomasherceg avatar Apr 26 '21 09:04 tomasherceg

I also think that paging options should define some kind of "capabilities" - can go to previous page, can go to last page, can go to a page with specific index etc.

We should somehow distinguish between three states (because we may want to display the control but keep it disabled):

  • the paging options support going to the previous page and it can be done right now (Visible=true, Enabled=true)
  • the paging options support going to the previous page, but it's not available right now (because you are on the first page) (Visible=true, Enabled=false)
  • the paging options don't support going to the previous page (Visible=false)

If I bind the DataPager to a data set with paging options which only support going to a next page, only the Next button should be rendered (the other buttons won't even have the properties they need for their bindings, like NearPageIndexes).

tomasherceg avatar Apr 26 '21 09:04 tomasherceg

I can see a potential problem with <DataControls.TemplateProvider> I didn't realize before - the template provider may need to know concrete type of sorting and paging options, otherwise it won't be able to bind to anything except the universal methods in the base interfaces of sorting and paging options. It should be fine if we just call pager.GoToNextPage() (if there is a JS translation in static commands), but for example, we won't be able to apply custom CSS classes based on whether the current column is sorted in asc/desc order (because we won't see concrete SortExpression and SortDescending in actual sorting options).

tomasherceg avatar Apr 26 '21 09:04 tomasherceg

Command Property Load static command containing a lambda function that returns filled GridViewDataSet. Example: (DataSetOptions opt) => myService.MyLoad(opt,...).

Not sure if the binding is correct, how do we assign the data set?

  • It should be (DataSetOptions opt) => DataSet = myService.MyLoad(opt,...), but the function must return a complete data set (it will be sending the options back)
  • We might define some method with a JS translation to be able to populate the data set with just data returned from the function: (DataSetOptions opt) => DataSet.Populate(myService.MyLoad(opt))

In the future, I'd like to allow "appending" to the data set, e. g. for infinite scrolling, and also to have support for "sparse" datasets in combination with virtualization (basically, the data set will know from the beginning there are 10000 records, but it will load them on-demand based on the scroll position in the table). So maybe there should be some way for the server function to return just an array of items (and maybe some other metadata like a total number of rows). Maybe we can pass the options back (because they will probably already contain such kind of metadata), but I am not sure if we should return the entire data set object, or some other thing (options + results) from which the dataset will be populated.

tomasherceg avatar Apr 26 '21 09:04 tomasherceg

Command Property Load static command containing a lambda function that returns filled GridViewDataSet. Example: (DataSetOptions opt) => myService.MyLoad(opt,...).

Not sure if the binding is correct, how do we assign the data set?

Yes it is correct. the lambda returns the dataset and the loader deals with the returned dataset in javascript. If you want to append and things like that, I see it as a property on the loader control. Like so: Mode=Append or Mode=Infinite.

Mylan719 avatar Apr 26 '21 10:04 Mylan719

I like these changes. From the BusinessPack point of view, it would require some changes so the DataControls.TemplateProvider is used to access the templates.

I have a few questions I would like to ask:

When GridViewDataSet.RequestRefresh() is called from a static command the DataSetLoader for the data set is located.

So the DataSetLoader is used only when used from static command? What happens if I call any of the DataSet or Pager methods through a non-static (regular) command and the loader exists for it?

In the past, it has been very hard to customize DotVVM Pager and GridView controls. For this reason, we created DataControls.TemplateProvider. The property is inherited from parent elements. It can be set for instance in a master page on body and it will affect all grids and pagers in the application.

Would it be possible to override DataControls.Template provider on one page? For example, if I have two GridViews on one page and want one of them to be sortable and the not?

Pager, Sorter, Filter properties are moved to separate DTO under the Options property.

Would there be any client-side API for the Filter property?

mrnustik avatar Apr 27 '21 11:04 mrnustik

Related issue #1402

acizmarik avatar Jun 01 '22 10:06 acizmarik