dotvvm
dotvvm copied to clipboard
Better data controls - Design
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 theItems
property, same as before -
Pager
,Sorter
,Filter
properties are moved to separate DTO under theOptions
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 likeFirst()
,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 methodSort(...)
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 filledGridViewDataSet
. 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 onGridViewDataSet
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 I think that you have a mistake there next
and forwad
?
What's signature of Sorter.Sort
? Can I use it for multi criteria sort?
The signature of Sorter is not defined yet. There are several possibilities.
- ICommandBinding Sorter.Sort(string columnName)
- ICommandBinding Sorter.Sort(int index)
- 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.
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.
@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.
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).
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).
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.
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
.
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?
Related issue #1402