UXGridView Part 4: Data Editing the MVVM-way with Commands
In the previous post, Jimmy announced the latest CTP for UXGridView with the CUD (Create, Update and Delete) capability. In this post, I will provide the step-by-step instructions to work with these new features.
Enabling the CUD capability
First of all, let’s enable the CUD (Create, Update, Delete) capability in UXGridView. You can enable these features by simply setting the CanUserAdd, CanUserEdit, and CanUserDelete properties to True.
1 2 3 |
<Intersoft:UXGridView CanUserAdd="True" CanUserDelete="True" CanUserEdit="True"/> |
Notice that there will be a NewRow element at the top of the Grid when you set the CanUserAdd property to True.
Furthermore, you can also delete or edit the selected item(s) when the CanUserEdit or CanUserDelete properties are set to True.
To edit a row you can use either mouse or keyboard. The following list shows you some configuration that you can change to edit a record using mouse / keyboard.
To edit a row using mouse:
- EditMouseGesture
- Single Click
Directly begin edit to the current selected cell.
- Second Click
Begin edit to the current selected cell after the row is selected.
- Double Click
Directly begin edit to the current selected cell when double click fires.
- Single Click
To edit a row using keyboard:
- EditKeyGEditKeyGesture
- F2
Directly begin edit to the current selected cell using F2 key.
- Any Keystroke
Directly begin edit to the current selected cell from any key stroke.
- F2
- EnterKeyAction
- EnterEdit
Directly begin edit to the current selected cell using Enter key.
- MoveToNextRow
Move to next row (does not edit the cell).
- EnterEdit
- EditEnterKeyAction
- CommitAndMovetoNextRow
Commit the changes at current edited row and move to the next row.
- ExitEdit
Exit the cell edit of the current edited cell.
- MoveToNextEditableCell
Move to next editable cell. (Will move to next row’s cell when it reach the end of the row)
- CommitAndMovetoNextRow
Beside these options, you can also use several common keystroke as listed below during editing.
- Tab / Shift + Tab
To move to next / previous editable cell.
- Shift + Enter
Commit the changes at current edited row and stay in the current selection
- Delete
Delete the current selected record(s)
- Escape
Cancel current changes. If currently you are in an active edit cell, it will cancel the cell changes. If you are in current active edit row, it will cancel the row changes.
Handling the CUD operation
To handle the CUD operation, UXGridView provides several command-related properties that you can specify to execute a method depending on the actions. These command- properties are listed as follows:
- PrepareNewRowCommand
Called when you begin edit at the NewRow element. Used to initialized the NewRowItem.
- ValidateRowCommand
Validate the row before the row is committed.
- InsertRowCommand
Called when a new row is committed. You can directly save the changes and/or refresh the UXGridView if necessary.
- UpdateCellCommand
Called when the cell is committed.
- UpdateRowCommand
Called when an existing row is committed. You can directly save the changes and/or refresh the UXGridView if necessary.
- DeleteRowCommand
Called when a row is deleted. You can directly save the changes and / or refresh the UXGridView if necessary.
- RejectRowCommand
Called when the changes in the row is cancelled. This command is used to reject the changes in the data entity if required (such as in DevForce).
- SaveChangesCommand
Called when the save changes command is executed. You handle this command to save all the pending changes made in the UXGridView.
- RejectChangesCommand
Called when the reject changes command is executed. You handle this command to reject all the pending changes made in the UXGridView.
These command properties can be bound to your ViewModel using delegate command. Next, I will show you how to bind these commands along with some important properties that are necessary for the CUD operation.
Binding the CUD Commands to UXGridView using MVVM Pattern
Let’s create the ViewModel for our example. First, define the delegate commands and the required selection properties, then instantiate them in the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
using System.Collections; using Intersoft.Client.Framework; using Intersoft.Client.Framework.Input; using Intersoft.Client.UI.Data; using UXGridView.Samples.ModelServices; namespace UXGridView.Samples.ViewModels { public class ServerEditingViewModel : ServerSideOperationViewModel { public ServerEditingViewModel() : base() { this.DeleteRowCommand = new DelegateCommand(ExecuteDeleteRow); this.InsertRowCommand = new DelegateCommand(ExecuteInsertRow); this.PrepareNewRowCommand = new DelegateCommand(ExecutePrepareNewRow); this.UpdateCellCommand = new DelegateCommand(ExecuteUpdateCell); this.UpdateRowCommand = new DelegateCommand(ExecuteUpdateRow); this.RejectRowCommand = new DelegateCommand(ExecuteRejectRow); this.RejectChangesCommand = new DelegateCommand(ExecuteRejectChanges); this.SaveChangesCommand = new DelegateCommand(ExecuteSaveChanges); this.ValidateRowCommand = new DelegateCommand(ExecuteValidateRow); } #region Fields private bool _hasChanges; private bool _isRefreshed; private object _newProduct; private object _selectedProduct; private IEnumerable _selectedProducts; #endregion #region EditableProductsSource private IEditableDataRepository EditableProductsSource { get { return this.ProductsSource as IEditableDataRepository; } } #endregion #region Selection and Editing Properties public object NewProduct { get { return this._newProduct; } set { if (this._newProduct != value) { this._newProduct = value; this.OnPropertyChanged("NewProduct"); } } } public object SelectedProduct { get { return this._selectedProduct; } set { if (this._selectedProduct != value) { this._selectedProduct = value; this.OnPropertyChanged("SelectedProduct"); } } } public IEnumerable SelectedProducts { get { return this._selectedProducts; } set { if (this._selectedProducts != value) { this._selectedProducts = value; this.OnPropertyChanged("SelectedProducts"); } } } public bool IsRefreshed { get { return this._isRefreshed; } set { if (this._isRefreshed != value) { this._isRefreshed = value; this.OnPropertyChanged("IsRefreshed"); } } } public bool HasChanges { get { return _hasChanges; } set { if (_hasChanges != value) { _hasChanges = value; OnPropertyChanged("HasChanges"); } } } #endregion #region Commands public DelegateCommand DeleteRowCommand { get; set; } public DelegateCommand InsertRowCommand { get; set; } public DelegateCommand PrepareNewRowCommand { get; set; } public DelegateCommand UpdateCellCommand { get; set; } public DelegateCommand UpdateRowCommand { get; set; } public DelegateCommand RejectRowCommand { get; set; } public DelegateCommand RejectChangesCommand { get; set; } public DelegateCommand SaveChangesCommand { get; set; } public DelegateCommand ValidateRowCommand { get; set; } #endregion } } |
Next, we will bind these commands to the UXGridView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
<Intersoft:UXPage xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:Intersoft="http://intersoft.clientui.com/schemas" xmlns:IntersoftModel="clr-namespace:Intersoft.Client.Data.Component<br /> Model.CollectionViews;assembly=Intersoft.Client.Data.ComponentModel" xmlns:ViewModels="clr-namespace:UXGridView.Samples.ViewModels" x:Class="UXGridView.Samples.Views.ServerSideOperation.Editing" Title="Editing Page" d:DesignWidth="1024" d:DesignHeight="800"> <Grid x:Name="LayoutRoot"> <Grid.DataContext> <ViewModels:ServerEditingViewModel/> </Grid.DataContext><br /> <Intersoft:DockPanel> <Intersoft:UXGridView AutoGenerateColumns="False" QueryOperation="Server" CanUserPage="True" PageSize="20" RowHeaderVisibility="Visible" IsBusy="{Binding IsBusy, Mode=TwoWay}" IsRefreshed="{Binding IsRefreshed, Mode=TwoWay}" ItemsSource="{Binding Products}" SortDescriptors="{Binding QueryDescriptor.SortDescriptors, Mode=TwoWay}" PageDescriptor="{Binding QueryDescriptor.PageDescriptor}" GroupFootersVisibility="Visible" GroupByBoxVisibility="Visible" CanUserAdd="true" CanUserDelete="true" CanUserEdit="true" NewItem="{Binding NewProduct, Mode=TwoWay}" SelectedItem="{Binding SelectedProduct, Mode=TwoWay}" ValidateRowCommand="{Binding ValidateRowCommand}" InsertRowCommand="{Binding InsertRowCommand}" DeleteRowCommand="{Binding DeleteRowCommand}" PrepareNewRowCommand="{Binding PrepareNewRowCommand}" UpdateCellCommand="{Binding UpdateCellCommand}" UpdateRowCommand="{Binding UpdateRowCommand}" SaveChangesCommand="{Binding SaveChangesCommand}" RejectRowCommand="{Binding RejectRowCommand}" RejectChangesCommand="{Binding RejectChangesCommand}" HasChanges="{Binding HasChanges}"> <Intersoft:UXGridView.GroupDescriptors> <Intersoft:UXGridViewGroupDescriptor PropertyName="CategoryID"/> </Intersoft:UXGridView.GroupDescriptors> <Intersoft:UXGridView.Columns> <Intersoft:UXGridViewTextColumn Header="Category ID" Binding="{Binding CategoryID}"/> <Intersoft:UXGridViewTextColumn Header="Product ID" Binding="{Binding ProductID}" IsReadOnly="True" Aggregate="Count" FooterFormatString="Count = {0}"/> <Intersoft:UXGridViewTextColumn Header="Product Name" Binding="{Binding ProductName}"/> <Intersoft:UXGridViewTextColumn Header="Units In Stock" Binding="{Binding UnitsInStock}" Aggregate="Max" FooterFormatString="Max = {0}"/> <Intersoft:UXGridViewTextColumn Header="Unit Price" Binding="{Binding UnitPrice}" Aggregate="Avg" FooterFormatString="Avg = {0:n2}"/> <Intersoft:UXGridViewTextColumn Header="Units On Order" Binding="{Binding UnitsOnOrder}" Aggregate="Min" FooterFormatString="Min = {0}"/> <Intersoft:UXGridViewTextColumn Header="Quantity Per Unit" Binding="{Binding QuantityPerUnit}"/> </Intersoft:UXGridView.Columns> </Intersoft:UXGridView> </Intersoft:DockPanel> </Grid> </Intersoft:UXPage> |
Handling the CUD Operation in ViewModel Using DevForce
After the commands are bound to the ViewModel, it is up to you how you want to handle the CUD operation.
If you prefer to automatically update the records after each CUD operation, you can do that in the InsertRowCommand, UpdateRowCommand and DeleteRowCommand respectively, and probably followed up by RefreshCommand to refresh the data. However, if you prefer a batch update, you can notify the UXGridView by setting the HasChanges property to True, and later call the SaveChanges method to perform the batch update.
This batch update capability might not be available in all data providers such as WCF RIA. When you enable server query in WCF RIA such as paging, sorting, and filtering; you always get a new fresh data from the database regardless of the changes in the client. This behavior is due to the WCF RIA not supporting client-side caching. In this case, you might want to do automatic update and/or refresh after each CUD operation. There are samples that show how to do this in our CTP package.
Next, I will show you how to handle the CUD Operation in the ViewModel. To save time, I will only cover the one using DevForce which allows you to enable batch update.
Since the sample used ProductsRepository to encapsulate all data operation, I’ll show you first what I’m doing in the ProductsRepository.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
using System; using System.Collections; using System.ComponentModel; using IdeaBlade.EntityModel; using IdeaBlade.Validation; using Intersoft.Client.Data.ComponentModel; using Intersoft.Client.Data.Provider.DevForce; using UXGridView.Samples.Data.DevForce; namespace UXGridView.Samples.ModelServices { public class ProductsRepository : IEditableDataRepository { public ProductsRepository(NorthwindEntities entityManager) { this.Manager = entityManager; } private static IDataRepository _repository; public static IDataRepository Instance { get { return _repository ?? (_repository = CreateRepository()); } set { _repository = value; } } private NorthwindEntities Manager { get; set; } public static IDataRepository CreateRepository() { return new ProductsRepository(EntityManager.Create()); } public void GetData(Action<IEnumerable> onSuccess, <br /> Action<Exception> onFail) { this.Manager.Products .ExecuteAsync( op => { if (op.CompletedSuccessfully) { if (onSuccess != null) onSuccess(op.Results); } else { if (onFail != null) { op.MarkErrorAsHandled(); onFail(op.Error); } } } ); } public void GetData(QueryDescriptor queryDescriptor, Action<IEnumerable> onSuccess, Action<int> onItemCountRetrieved, Action<Exception> onFail) { this.Manager.Products.OrderBy(p => p.ProductID).Parse(queryDescriptor) .ExecuteAsync( op => { if (op.CompletedSuccessfully) { if (onSuccess != null) onSuccess(op.Results); if (onItemCountRetrieved != null) onItemCountRetrieved(-1); // not applicable; } else { if (onFail != null) { op.MarkErrorAsHandled(); onFail(op.Error); } } } ); } public void GetTotalItemCount (QueryDescriptor queryDescriptor, Action<int> onSuccess) { var op = this.Manager.Products .Parse(queryDescriptor, false).AsScalarAsync().Count(); op.Completed += (o, e) => { if (onSuccess != null) onSuccess(e.Result); }; } public void Insert(object entity) { this.Manager.AddEntity(entity); } public void Delete(IList entities) { foreach (object o in entities) { this.Delete(o); } } public void Delete(object entity) { Product product = entity as Product; product.EntityAspect.Delete(); } public void RejectChanges() { this.Manager.RejectChanges(); } public void SaveChanges( Action onSuccess, Action<Exception> onError) { this.Manager.SaveChangesAsync ( op => { if (op.IsCompleted) { if (op.HasError) { // handle error op.MarkErrorAsHandled(); onError(op.Error); } else { onSuccess(); } } }, null ); } public object Create() { return this.Manager.CreateEntity<Product>(); } public void Validate(object entity) { Product product = (Product)entity; product.EntityAspect.ValidationErrors.Clear(); product.EntityAspect.VerifierEngine.Execute(product); if (product.CategoryID < 1 || product.CategoryID > 8) product.EntityAspect.ValidationErrors .Add(new VerifierResult( VerifierResultCode.Error, "Specified CategoryID does not exist", new string[] { "CategoryID" })); if (product.UnitPrice < 0) product.EntityAspect.ValidationErrors .Add(new VerifierResult (VerifierResultCode.Error, "Unit Price can not be less than 0", new string[] { "UnitPrice" })); if (product.UnitsInStock < 0) product.EntityAspect.ValidationErrors .Add(new VerifierResult( VerifierResultCode.Error, "Units in Stock can not be less than 0", new string[] { "UnitsInStock" })); if (product.UnitsOnOrder < 0) product.EntityAspect.ValidationErrors .Add(new VerifierResult( VerifierResultCode.Error, "Units on Order can not be less than 0", new string[] { "UnitsOnOrder" })); } public void RejectChanges(object entity) { IRevertibleChangeTracking revertible = <br /> (IRevertibleChangeTracking)entity; <br /> revertible.RejectChanges(); } } } |
Finally, let’s handle the CUD commands in our ViewModel.
For Create Operation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void ExecuteInsertRow(object parameter) { this.NewProduct = null; if (!this.IsBatchUpdate) this.SaveChanges(); else this.HasChanges = true; } public void ExecutePrepareNewRow(object parameter) { // It's possible to initialize the new row with default values // Example: // product.ProductName = "New Product"; this.NewProduct = this.EditableProductsSource.Create(); this.EditableProductsSource.Insert(this.NewProduct); } |
For Update Operation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void ExecuteUpdateCell(object parameter) { object[] updateCellParameters = (object[])parameter; object product = updateCellParameters.GetValue(0); string property = updateCellParameters.GetValue(1).ToString(); // perform cell-level validation if required } public void ExecuteUpdateRow(object parameter) { if (!this.IsBatchUpdate) this.SaveChanges(); else this.HasChanges = true; } |
For Delete Operation
1 2 3 4 5 6 7 8 9 |
public void ExecuteDeleteRow(object parameter) { this.EditableProductsSource.Delete(parameter as IList); if (!this.IsBatchUpdate) this.SaveChanges(); else this.HasChanges = true; } |
For Validation, Reject Row and Refresh Operation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public void SaveChanges() { this.IsBusy = true; this.EditableProductsSource.SaveChanges ( () => { this.IsRefreshed = true; this.LoadProducts(); // refresh this.HasChanges = false; this.IsBusy = false; }, exception => { this.IsBusy = false; MessagePresenter presenter = new MessagePresenter(); presenter.ShowErrorMessage("An unexpected error has occurred: " <br /> + exception.Message); } ); } public void ExecuteValidateRow(object parameter) { this.EditableProductsSource.Validate(parameter); } public void ExecuteRejectRow(object parameter) { if (parameter != null) this.EditableProductsSource.RejectChanges(parameter); this.NewProduct = null; } |
For Batch Operation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void ExecuteRejectChanges(object parameter) { this.EditableProductsSource.RejectChanges(); this.HasChanges = false; } public void ExecuteSaveChanges(object parameter) { // if users click on the save changes while the new row is being edited, // presume the new row isn't intended if (this.NewProduct != null) this.EditableProductsSource.RejectChanges(this.NewProduct); this.SaveChanges(); } |
I hope this post gives you a comprehensive understanding on handling the CUD (Create, Update, Delete) operation in UXGridView using MVVM design pattern. In the next post, I will blog about customizing editing controls in UXGridView, for instance, customizing the editing control of certain columns to more advanced input controls such as UXDateTimePicker, UXNumericUpDown, UXSliderBar, and so forth.
If you have any questions regarding the CUD operation, please post some questions in our community website.
Regards,
Andry
Comments