Data Moving Plugin Controls
The Data Moving Plug-in is in its RC stage and will be released to the market within about one month. This is the first of 6 introductory tutorials with associated videos about it.
All Controls provided in the Data Moving Plug-in are compatible with both mouse based devices and with touch devices. Moreover all controls are accessible, and the user can interact with them with the only support of the keyboard.
From a conceptual point of view all controls can be classified into two big categories: Client Controls and Server Controls. Client Controls are bound to client side javascript data and create their Html dynamically on the client side, while the Html of Server Controls is rendered on the server side using data contained in a server side ViewModel.
The main advantages of server controls is that their ”static” Html is visible to the search engines, and that they require less “job” on the client side so they are efficiently rendered also by low performance browsers (such as the browsers of low quality mobile devices). On the other side client controls offer more flexibility, and a better interaction with all other html elements of the page.
The Data Moving Plug-in contains both type of controls, in order to fit better the developers needs. Typically, server side controls communicate with the server through form submits, or through ajax posts that return new Html. Below a grid that filters, pages,sorts and updates data through form submits:
Item editing may be performed either on line (on the rows themselves) or through a detail window that appears when the user clicks the edit button that is on the left of each row:
A similar windows is used also to filter data.
All these features come at 0 cost since they require no coding. We just need to specify the services required through a fluent interface:
- .DeclareQueryResolver(Html, m => m.PreviousQuery, enablePrevPage: true, enablePageSize:true)
- .CreateDynamicSubmitRetrievalManager("GridExample.QueryModel.RM", actionUrl: Url.Action("IndexQuery", "GridsDemo"), newPrefix: "")
- .DetailFilterBuilder(out detailBuilder)
DeclareQueryResolver specifies the part of the server side ViewModel where to store all query information (sorting, filtering, etc). The Data Moving Plugin offers the QueryViewModel<T> pre-defined class for this job but the user may choose to configure a custom class. We need just to add this class to our ViewModel, and the Data Moving Plug-in takes care of everything else. The services required (filtering, paging, sorting) are specified with optional arguments of the call: as default all services are enabled. The third argument of DeclareQueryResolver enables the user choice of the page size, while the second argument requires that the paging engine “remembers” the previous page, so that if a page change fails it can return to the previous page. Once we have enabled paging with DeclareQueryResolver, it is enough to add a pager column to a toolbar to have the a working paging service:
- .AddPagerColumn("FullCompletePager")
The name passed as argument to the pager column is a reference to a pager “prototype” we defined in a configuration class.
In fact, in order to simplify controls coding and to allow reusability of code, the developer has the option to store controls “options” in a configuration class by defining both controls “prototypes”, and row “prototypes”. Prototypes, may contain all control or row settings that are not specific for a particular data item.
Since, we required user choice of the page size, the pager will automatically show also a textbox to enter the page size.
The CreateDynamicSubmitRetrievalManager methods gives information on how to send queries to the server. The first parameter is a javascript property where to store the RetrievalManager javascript object that will take care of managing all queries-related communication with the server(this is useful if we would like to customize its behavior on the client side). The second parameter is the url of the action method that processes the query, and finally the last argument specifies a possible prefix to add to the quey ViewModel. This parameter is useful if the model accepted by the action method is not just a QueryViewModel<T>, but a more complex model containing QueryViewModel<T> in one of its properties. In our case (and in most of the cases) the prefix must be empty.
The DetailFilterBuilder method provides an object we may use to render the edit/detail window. For more information about edit/detail windows please refer to Data Moving Plugin Forms: Detail Views, Data Filtering UI, and Undo/Redo Stack.
On the server side the query processing is straightforward:
- public ActionResult IndexQuery(QueryViewModel<HumanResource> query) //read-edit simple server grid
- {
- prepareData();
- IEnumerable<HumanResource> res = fakeDB.Get();
- if (query != null)
- {
- res = query.ApplyToIEnumerable(res, true, true, pageSize);
- }
- var model = new TrackedGridDemoViewModel { Staff = res.Select(m => new Tracker<HumanResource> { Value = m, OldValue = m }), PreviousQuery = query, ViewSelector = null };
- return View(selectViewTracked(model.ViewSelector), model);
- }
PrepareData just retrieve a fake Db (implemented with a dictionary) stored in the Session. The query received from the client is applied to the fake Table with a single call to ApplyToEnumerable. In the case of an actual DB we must use the method ApplyToQueryable, instead. Then all data are placed in the page ViewModel.
As you can see all items of the list obtained as result of the execution of the query are enclosed within a Tracker Object. The Tracker object enables Changes Tracking on the grid. There are two kind of server grids: the first one accepts directly a list of Tracker<T> objects and takes care automatically of changes tracking, the other one needs and adapter that adds the needed changes tracking capabilities. Below how to use the adapter:
- var h = Html.TrackedListRendering(m => m.Staff);
- var dBuilder = h.SimpleGrid(m => m.Values, false, true)
- .EnableAlternateStyle()
- ...........
As you can see the call to the adapter TrackedListRendering returns an Html helper that we can use to render the grid.
The user can add, delete and modify rows. When he hits the submit button, the whole form the Grid is enclosed in is submitted to the server. Below the Action Method that processes this submit:
- //Action methods to process item changes
- [HttpPost]
- public ActionResult TrackedIndex(TrackedGridDemoViewModel model)
- {
- prepareData();
-
- if (ModelState.IsValid)
- {
- ModelState.Clear();
- if (model != null)
- {
- IList<HumanResource> inserts, deletes, modified;
- model.Staff.GetChanges(out inserts, out deletes, out modified, m => { m.Id = Guid.NewGuid(); });
- fakeDB.Insert(inserts); fakeDB.Delete(deletes); fakeDB.Modify(modified);
- QueryViewModel<HumanResource> startQuery = model.PreviousQuery != null ? model.PreviousQuery : new QueryViewModel<HumanResource>().AddSortCondition(m => m.Surname, OrderType.Ascending);
- model = new TrackedGridDemoViewModel { ViewSelector = model.ViewSelector, Staff = startQuery.ApplyToIEnumerable(fakeDB.Get(), true, true, pageSize).Select(m => new Tracker<HumanResource> { Value = m, OldValue = m }), PreviousQuery = startQuery };
- }
-
- }
- else
- {
- if (model != null && model.PreviousQuery != null)
- {
- model.PreviousQuery.Page = model.PreviousQuery.PreviousPage;
- model.PreviousQuery.SortExpression = model.PreviousQuery.PreviousSortExpression;
- }
- }
- return View(selectViewTracked(model.ViewSelector), model);
- }
Since we enclosed our items within Tracker objects we are entitled to call the GetChanges method that computes all changes in the IEnumerable items and returns the three lists of all inserted, deleted and modified items. The fourth argument of the GetChanges method, if provided, must be am Action that is applied to all inserted elements. Typically it is used to add a principal key to all Inserted elements (when this is possible before accessing the database).
In case we use a Client Side Grid the first step is to move a part of the Server Side ViewModel to the client side, and to get an Html helper that has the capabilities to render client side controls. Both tasks are accomplished with a simple call to the SendToClient method:
- @{var h = Html.SendToClient(m => m.ClientModel, "GridDemo.ViewModel");}
The second argument just says where to store the Client Side ViewModel.
Now we may use h to specify all options of our grid and to render it:
- var dBuilder = h.SimpleGrid(m => m.StaffToShow, true, true, hasRowDisplayAndRowEdit: true).......
Since our client side grid uses OData for the queries the method calls that enable the query engine are a little bit different:
- .CreateStandardQueryResolver(Model.PreviousQuery, enablePrevPage: true, enablePageSize: true)
- .StartODataRetrievalManager(false, "GridExample.QueryModel.RM", (uint)Model.PreviousQuery.PageSize, actionUrl: Url.HttpRouteUrl("DefaultApi", new { httproute = "", controller = "GridsDemoApi"}))
In this case we call the CreateStandardQueryReoslver method instead of DeclareQueryReolver because the server side models doesn’t need to contain any object to store query information, since all query handling is done on the client side. Accordingly an instance of QueryViewModel<T> is just created and inserted in the Client Side ViewModel in a way that is completely transparent to the developer. However, in our case we add an instance of QueryViewModel<T> also to the server side ViewModel: this instance of QueryViewModel<T> doesn’t take part to any query processing but we use it just to initialize the client side query by passing it as first parameter to the CreateStandardQueryReoslver.
StartODataRetrievalManager specifies the use of a RetrievalManager that is able to communicate with OData points. This javascript RetrievalManger object is stored in the GridExample.QueryModel.RM property. The first argument of StartODataRetrievalManager set to false says we dont want to issue an initial query immediately after the page is rendered because the initial data have already been passed in the initial Client Side ViewModel. The third argument just says the initial page size to use for paging: this value will probably change during the grid operation since we enabled user choice of the page size. Finally, the last argument is the url of the WebApi controller that processes the queries.
Client grids are able to send items changes to the server with a simple form submit in exactly the same way a server grid would do, but the preferred way to communicate with the server is by sending data in json format with an ajax call. This job is done by the updatesManager javascript class that takes care of computing changes on the client side and to send Inserted, updated, and deleted items to the server in json format (just the keys of the deleted elements are sent to the server).
No javascript coding is required to use an updatesManager class; we need just to declare it with a fluent interface:
- .CreateUpdatesManager("UM").BasicInfos(m => m.Id).IsRoot(Url.HttpRouteUrl("DefaultApi", new { httproute = "", controller = "GridsDemoApi" })).EndUpdatesManager()
UM is the name of the property of the Client Side ViewModel where to store the newly created updatesManager class, m => m.Id specifies the principal key of the items. Finally, the IsRoot call specifies that this is a root updatesManager that takes care of “interfacing” directly the controller. In fact, there might be also children updatesManagers that takes care of the children of a one-to-many relation, and that delegate the communication with the server to the updatesManager of the father items. The only argument of IsRoot is the url of the action method that processes the updates. Children updatesManagers are discusses in this video.
The controller receives a standard OpenUpdater<M,K> model where M is the type of the items and K is the type of their principal key. In case the page contains several grids and/or several type of entities the controller may receive a complex model containing several OpenUpdater<M,K> inside different properties and possibly also other data the client needs to send to the server. For more information on complex updates please refer to Single Page Applications 1: Manipulating the Client Side ViewModel.
Processing of the OpenUpdater<M,K> is done quite automatically by the ApiResponseBuilder class that takes care of building a response in the format accepted as response by the updatesManger. This response might contain either validation errors or the keys of the Inserted elements computed by the server. In case of a complex model with several OpenUpdater<M,K> the ApiResponseBuilder is able to build a complex response whose parts will be dispatched adequately once the response reaches the client side. The way ApiResponseBuilder handles complex models is detailed in Single Page Applications 1: Manipulating the Client Side ViewModel.
In our simple case:
- public HttpResponseMessage Post(OpenUpdater<HumanResource, Guid?> model)
- {
- prepareData();
- //ModelState.AddModelError("", "test error");
- if (ModelState.IsValid)
- {
- try
- {
- var builder = ApiResponseBuilder.NewResponseBuilder(model, ModelState, true, "Error in client request");
- builder.Process(m => m, m => m.Id);
- //call to business layer
-
- fakeDB.Insert(model.Inserted); fakeDB.Delete(model.Deleted); fakeDB.Modify(model.Modified);
- //
- var response = builder.GetResponse();
- return response.Wrap(this.Request);
- }
- catch (Exception ex)
- {
- ModelState.AddModelError("", ex.Message);
- return
- Request.CreateResponse(System.Net.HttpStatusCode.InternalServerError,new ApiServerErrors(ModelState));
- }
- }
- else return
- Request.CreateResponse(System.Net.HttpStatusCode.InternalServerError, new ApiServerErrors(ModelState));
- }
In case there is no error we create an ApiResponseBuilder object with a call to NewResponseBuilder. We pass to this object both, the whole model, the ModelState and a default error message to be used in case of hill formed requests. The third argument set to true, require to throw an exception in case of hill formed requests.
The Process method performs all needed key processing for the newly inserted elements. In our case the processing is simple since we have no one-to-many relations, so we need just to provide principal keys to the newly inserted elements. The first argument selects what OpenUpdater<M,K> to process (this argument is needed to handle also complex models with several OpenUpdater<M,K>); in our case it is trivial: m => m. The second argument selects the principal key of the items contained in the selected OpenUpdater<M,K>. A function that receives as input an item and returns a newly computed principal key for it may be passed as third optional argument. Since we have not provided this function, the default Guid key generator is used.
In case of errors we create an ApiServerErrors object that extracts automatically all errors from the ModelState and put them into an Api response with the Internal Server Error header.
In case the receiving controller is not a WebApi controller but a standard Mvc controller we have to substitute the ApiResponseBuilder class with the ResponseBuilder class and the ApiServerErrors class with the ServerErrors class. Moreover the response must be returned directly with return Json(response), and the errors with return Json(new ServerErrors(ModelState)).
The way to style and decide the appearance of both server and client grids is substantially the same. We may specify options with a fluent interface, add toolbars, and define data rows options.
Both toolbars and data rows are composed of columns that are arranged according to a row template. The developer may choose between three pre-defined row templates:
- A <table> based template that may be used in case of tabular data.
- A <div style=”display: table”…based template if we need a table like appearance but the data are not “tabular”
- A div/float based template.
Moreover, the developer can furnish also its own custom row template, that either arranges the pre-existing column definitions or renders directly the item without taking care of column definitions. There are different types of columns:
- Standard columns. Their default template may be controlled with a fluent interface. They are able to render properly any .Net data type both in edit and in display mode. We may choose to use the Mvc Display and Edit templates defined for the given types, or Display only, and TypedTextBoxes based templates that are automatically optimized for each data type. It is also possible to specify the use of the new Html5 inputs; the selection of the input type is done according to the column data type and to a possible DataType attribute that may decorate the property associated to the column.
- Image columns. They extracts both the src and the alt of the image from the item data;
- Link columns capable of extracting both the href an the link text from the item data.
- Text Columns, to render constant text or constant Html.
- Button Columns, to render single buttons or buttons organized in bars.
- Pager Columns, to display a pager.
- RowCount columns, to count rows
- Error Column, to display errors associated to the whole row.
All columns have standard Header, Footer, edit, Display, Edit Detail, Display Detail, and Filter templates that can be customized with a fluent interface, but for each of them the developer may provide a custom template.
Below the definition of a toolbar that contains a a button that opens the filter data dialog, and a pager:
- .StartToolbar(false)
- .RowContainerLevel(ItemContainerLevel.Row)
- .RowHeight("3em", true)
- .AddButtonToolbar(ShowButtonIn.Both, CommandButtonType.FilterWindow).ColumnWidth(40).EndButtons()
- .AddPagerColumn("FullCompletePager")
- .EndToolBar()
Below the definition of a data row:
- .AddRowType(true, true)
- .DeclareQueryResolver(Html, m => m.PreviousQuery, enablePrevPage: true, enablePageSize:true)
- .CreateDynamicSubmitRetrievalManager("GridExample.QueryModel.RM", actionUrl: Url.Action("IndexQuery", "GridsDemo"), newPrefix: "")
- .DeclareDisplayStore()
- .DetailFilterBuilder(out detailBuilder)
- .RowContainerLevel(ItemContainerLevel.Row)
- .DeclareInvisibleInnerRowContainer()
- .HtmlTitle(ShowTitleIn.Display | ShowTitleIn.Edit)
- .RowHeight("3em", true)
- .AddRowCountColumn(false, width: 4, widthIsPercentage: true)
- .AddButtonColumn(ShowButtonIn.Both, CommandButtonType.EditWindow, "GridExample.selectItem GridExample.unselectItem").ColumnWidth(4, true).HorizontalAlign(MVCControlsToolkit.Controls.Core.HAlign.center).EndButtons()
- .StartColumn(t => t.Name, width: 46, widthIsPercentage: true).Queries(true, true).HorizontalAlign(MVCControlsToolkit.Controls.Core.HAlign.center).EndColumn()
- .StartColumn(t => t.Surname, width: 46, widthIsPercentage: true).Queries(true, true).HorizontalAlign(MVCControlsToolkit.Controls.Core.HAlign.center).EndColumn()
- .AddColumn(t => t.Address, true)
- .AddImageColumn(t => t.AvatarAlt, t => t.AvatarUrl, new { style = "width: 100px; height: 100px;" }, true)
- .StartColumn(t => t.Id).DeclareHidden().EndColumn()
- .EndRowType();
It contains the query related methods we already discussed before, the DeclareDisplayStore() method call that require that the rows will work just in display mode, but that they store fields values in hidden fields, so that the row content can be edited with the help of an edit window.
The HtmlTitle() method requires to add an Html title attribute to all columns with the same content of the column field (useful if the content of the field is not fully visible, but it appears “dotted”). There is a RowCountColumn, and a ButtonColumn that opens an edit row dialog.
Then, we have the Name and Surname field columns. Both of them make the call Queries(true, true), that enables both sorting and filtering on them.
Interesting also the Image column for the avatar. It extracts both the src, and the alt from the item. The last argument set to true specifies that the image column must appear just in the detail window.
Finally, The column for the principal key is declared hidden because we don’t want to show the Id, but just to store it into an hidden field.
The Data Moving Plug-in includes 4 different types of grids(two are server side controls and the other two are client side controls), a TreeGrid/TreeView, and a menu control. The TreeGrid/TreeView working is shown in this video. The menu control may be displayed both horizontally and vertically; it works both with mouse hover or with mouse click (or touch in case of touch devices). Moreover, also all menu items use the Data Moving Plug-in row templates engine we described before for grids.
The Data Moving Plugin contains also a powerful Form Control, with undo/redo capabilities, and a DualSelect to select items from a pre-existing list, that are able to work both as server side controls and as client side controls. Both of them are described in Data Moving Plugin Forms: Detail Views, Data Filtering UI, and Undo/Redo Stack and in its associated video.
Included in the plug-in also powerful knockout bindings for jqPlot and tinyMce. Moreover, tinyMce has been enhanced with a “documents template engine” that give us the possibility to define document templates containing “variables” that can be instantiated either with the content of other input fields or with the content of javascript data structures. This video shows both the enhancements provided to jQplot, and tinyMvce and the documents template engine.
Finally, the developer has access also to all Mvc Controls Toolkit controls, such as TypedTextBox, TypedEditDisplay, DateTimeInput, etc. They can me mixed with all Data Moving Plug-in controls and they can be used also in all template definitions.
Below a video that shows a review of all Data Moving Plug-in Controls:
That’ all for now!
Stay tuned and give a look also to all other Data Moving Plugin introductory tutorials and videos
Francesco
Tags: DataGrid, Mvc, MVC Controls, Data Moving, Grid, MVC Controls Toolkit, MVC Helpers