This tutorial describes the code used to implement a member search and results viewer for a client of mine. They needed a Members Listing Page to display a list of members. The listing page contains a set of text boxes for the user to enter search criteria and a grid to display the results. The client needed to have the ability to search for members by social security number, membership card number, employee number, and/or either a whole or part of a last or first name. Multiple search criteria could be entered, but because the database contained thousands of members, it was necessary to ensure that at least one of the fields was entered.
I am not an employee of either Intersoft or IdeaBlade. I am an independent developer who uses these tools on a daily basis to be more productive with my time.
-William H. Gower
Here is a screen shot of the finished product:
This tutorial will consist of four sections. The first section will describe the creation of the Members Listing page, the view, the second, the creation of the view model class to handle the commands and the code, the third section will how to tie the view and view model classes together and the final section will discuss the repository classes.
This tutorial will use the Model View View Model (MVVM) framework. If you are not familiar with this framework or want additional information, just google MVVM and you will find a wealth of information on the subject.
I will be using the Intersoft ClientUI 5 tools for the development of the views and the binding of the commands on the view to the view model. I will use IdeaBlade DevForce 6.1.1 to create the entity framework and provide access to the database. I will provide the entire source code as an attachment but for the purposes of the tutorial discuss only code and xaml pertinent to the tutorial at hand.
Since this is a limited tutorial, a lot of preliminary work is already assumed to have been done. It is assumed that a project was created using an MVVM template provided by Intersoft. It is assumed that IdeaBlade DevForce for Silverlight was used to create the entity framework. Here is the structure of the solution that I will be using.
The following is an explanation of each of the folders and files above.
The view is developed using a UXPage. It represents a page in a browser or a screen in an out of browser application. Notice that the Members Listing UXPage actually sits inside of Main Page. The Main Page consists of the header and a ItemControl that runs vertically on the left hand side of the UXPage. The right hand side is the content holder for all the various listing classes. In the case of this application, there is an Inventory listing, a Sales Invoice listing etc. The listing page is for locating records to work with while the actual data entry will take place using dialog boxes.
I added a new UXPage to the project and named it MembersListView.cs. I divided the page into five parts, the header, the toolbar, the criteria panel, the grid panel, and the status bar at the bottom of the screen. There are many ways to code this in xaml. My way may or may not be the best, but it works.
I start by creating a DockPanel using the Intersoft:DockPanel. It will be the container for entire screen. I then use a GlassLabel control to display the header.
Intersoft | <Grid x:Name="LayoutRoot"> <Intersoft:DockPanel FillChildMode="Custom"> <Intersoft:GlassLabel Intersoft:DockPanel.Dock="Top" Content="Members Listing" VerticalAlignment="Top" Height="50" BorderThickness="0" CornerRadius="{Binding ElementName=membersViewPage, Path=HeaderCornerRadius}" FontSize="21.333"/> |
I create a toolbar using the UXToolBar class. Ignore the commands at this point, they will be explained in a later part of the tutorial.
Intersoft | <Intersoft:UXToolBar Intersoft:DockPanel.Dock="Top" CornerRadius="0" GripHandleVisibility="Collapsed" Height="30" OverflowHandleVisibility="AsNeeded"> <Intersoft:UXToolGroup MinWidth="450"> <Intersoft:UXToolBarButton Command="{Binding AddCommand}" Content="Add New Member" ToolTipService.ToolTip="Add a new member (Ctrl + Shift + A)"/> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding EditCommand}" Content="Edit Member" ToolTipService.ToolTip="Edit selected member (Ctrl + Shift + E)" /> <Intersoft:UXToolBarButton Command="{Binding EditCommand}" Content="View Member" ToolTipService.ToolTip="View selected member (Ctrl + Shift + V)" /> <Intersoft:UXToolBarButton Command="{Binding DeleteCommand}" Content="Delete Member" ToolTipService.ToolTip="Delete selected member (Ctrl + Shift + X)"/> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding FindCommand}" Content="Find Member" ToolTipService.ToolTip="Find member" /> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding ShowReportsCommand}" Content="Member Reports" ToolTipService.ToolTip="" /> </Intersoft:UXToolGroup> </Intersoft:UXToolBar> |
Intersoft | <Intersoft:GroupBox Width="900" Header="Search Criteria" Height="135" Intersoft:DockPanel.Dock="Top" CornerRadius="25"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100" /> <ColumnDefinition /> <ColumnDefinition Width="100" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Margin="0,5,0,5">SSN:</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" HorizontalAlignment="Right" Margin="0,5,0,5">Employee #:</TextBlock> <TextBlock Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Margin="0,5,0,5">Card #:</TextBlock> <TextBlock Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" Margin="0,5,0,5">Last Name:</TextBlock> <TextBlock Grid.Row="1" Grid.Column="2" HorizontalAlignment="Right" Margin="0,5,0,5">First Name:</TextBlock> <Intersoft:UXTextBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" Margin="5,0,0,0" Width="150" Height="25" Text="{Binding MemberQuery.SSN, Mode=TwoWay}" /> <Intersoft:UXTextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Margin="5,0,0,0" Width="150" Height="25" Text="{Binding MemberQuery.EmployeeNumber, Mode=TwoWay}"/> <Intersoft:UXTextBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" Margin="5,0,0,0" Width="150" Height="25" Text="{Binding MemberQuery.CardNo, Mode=TwoWay}"/> <Intersoft:UXTextBox Grid.Row="0" Grid.Column="3" HorizontalAlignment="Left" Margin="5,0,0,0" Width="200" Height="25" Text="{Binding MemberQuery.LastName, Mode=TwoWay}" /> <Intersoft:UXTextBox Grid.Row="1" Grid.Column="3" HorizontalAlignment="Left" Margin="5,0,0,0" Width="200" Height="25" Text="{Binding MemberQuery.FirstName, Mode=TwoWay}" /> </Grid> </Intersoft:GroupBox> |
Intersoft | <Intersoft:GroupBox Width="900" Height="540" Intersoft:DockPanel.Dock="Top" CornerRadius="25"> <Intersoft:UXGridView AutoGenerateColumns="False" Name="MemberGrid" ItemsSource="{Binding Members}" QueryOperation="Server" SortDescriptors="{Binding QueryDescriptor.SortDescriptors, Mode=TwoWay}" IsBusy="{Binding IsBusy}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" SelectionMode="Single" CanUserSortColumns="True"> <Intersoft:UXGridView.Columns> <Intersoft:UXGridViewTextColumn Header="Card #" Binding="{Binding CardNo}" Width="70" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="Empl #" Binding="{Binding EmployeeNumber}" Width="90" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="Last Name" Binding="{Binding LastName}" Width="170" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="First Name" Binding="{Binding FirstName}" Width="170" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="Address 1" Binding="{Binding Addr1}" Width="155" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="City" Binding="{Binding City}" Width="100" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="State" Binding="{Binding State}" Width="40" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> <Intersoft:UXGridViewTextColumn Header="Zip" Binding="{Binding Zip}" Width="80" CanUserSort="True"> <Intersoft:UXGridViewTextColumn.HeaderStyle> <Style TargetType="Intersoft:UXGridViewColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center"/> </Style> </Intersoft:UXGridViewTextColumn.HeaderStyle> </Intersoft:UXGridViewTextColumn> </Intersoft:UXGridView.Columns> </Intersoft:UXGridView> </Intersoft:GroupBox> |
I next create the columns in the grid providing the header text, what column in the Members object to use for binding, and also indicate whether that column can be sorted. It may not make sense to allow all columns in the grid to be sorted.
Finally I create the status bar which will display to the user the number of records displayed in the grid and which one if any of them has been selected.
Intersoft | <Border BorderBrush="Silver" BorderThickness="0" Intersoft:DockPanel.Dock="Bottom" Height="30" CornerRadius="{Binding ElementName=membersViewPage, Path=FooterCornerRadius}" Background="#FF2A2A2A"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Foreground="White" Text="{Binding Path=MembersCount}" VerticalAlignment="Center"/> <Intersoft:UXSeparator Orientation="Vertical" Height="16" Width="1" Margin="10,0"/> <TextBlock Foreground="White" Text="{Binding Path=SelectionStatus}" VerticalAlignment="Center" /> </StackPanel> </Border> </Intersoft:DockPanel> </Grid> |
In this first section, we looked at the view which was based on a UXPage. We created a search criteria panel and a grid panel and placed controls in the panels. At this point if you compiled this project and tried to run it, the view would display similar to the display above but nothing would happen as the commands on the view are not yet hooked up to the view model. In Section 2, we will create the view model and in section 3 hook up the commands.
The view holds all the objects that are displayed to the user. But the objects displayed do not do anything without some code attached to them. Because we are using MVVM, the controls reside in the View and the code to handle the commands and track the state will reside in the ViewModel class. We will create a new class in our project and name it MembersListViewModel.cs. Note the naming structure I use. The View is called MembersListView and the View Model is named MembersListViewModel. Okay I didn’t invent the naming structure, but I did think that it was a good idea to use in all my applications.
The view model will be derived from a class called ViewModelBase. ViewModelBase is extended from INotifyPropertyChanged. Every time a property in my View Model changes I will raise the OnPropertyChanged event and pass as a parameter the name of the property that was changed. The View attached to this View Model will be notified of the change and can redisplay the changed object on the view accordingly.
C# | public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } |
C# | private MemberViewModel _selectedItem; private bool _isInEditing; private bool _isBusy; private QueryDescriptor _queryDescriptor; private MemberQuery _memberQuery; |
The QueryDescriptor is a new class that was introduced in the previous release of ClientUI. Its purpose is to hold the query information that will be passed to the GetMembers function. Later in the tutorial we will take the fields from the search criteria panel and build the QueryDescriptor object. The QueryDescriptor will be passed to the GetMembers function and the entity framework will parse the QueryDescriptor and retrieve the appropriate records if any. Intersoft and IdeaBlade are closely working together as partners to ensure that the ClientUI controls work with the DevForce classes and integrate well. The MemberQuery object holds the criteria data filled in on the View.
Create a new class named MemberQuery.cs and save it to the Helpers folder
C# | public class MemberQuery { string _ssn = string.Empty; string _lastName = string.Empty; string _firstName = string.Empty; string _employeeNo = string.Empty; public string SSN { get { return _ssn; } set { _ssn = value; } } public string LastName { get { return _lastName; } set { _lastName = value; } } public string FirstName { get { return _firstName; } set { _firstName = value; } } public string EmployeeNo { get { return _employeeNo; } set { _employeeNo = value; } } public Int32? CardNo { get; set; } } |
Let’s add several properties to the class and then instantiate them in the constructor.
C# | public ITicketWareRepository Repository { get; set; } public ObservableCollection<MemberViewModel> Members { get; set; } public IMessagePresenter MessagePresenter { get; set; } public MembersListViewModel() { this.Members = new ObservableCollection<MemberViewModel>(); this.Members.CollectionChanged += Member_CollectionChanged; this.Repository = TicketWareRepository.Instance; this.MessagePresenter = new MessagePresenter(); _memberQuery = new MemberQuery(); } |
The Members object is the collection that will hold the members returned from the data source and stored in the grid. Notice that both the Members object and the _selectedItem object are an instance of the MemberViewModel class and not the Member class. The MemberViewModel class represents the columns of the member class that will be displayed in the grid. I can also create custom properties in the MemberViewModel class to represent columns from related tables or derived columns. For example, I have two tables Inventory and Vendor. Inventory is related to Vendor because it contains the vendor id of the vendor for that product. So there is a one to many- relationship between Vendor and Inventory. In a grid in which I am displaying the inventory rows I would also like to display the vendor name not just the vendor id. In my InventoryViewModel class I would create a property as follows:
C# | public string VendorName { get { return _inventory.Vendor.VendorName; } } |
Here is the code for the MemberViewModel. I need to create a property for every column I wish to display in the grid.
C# | public class MemberViewModel : ViewModelBase { // Fields private Member _member;// // Constructor public MemberViewModel(Member member) { _member = member; }// // Views public Member Member { get { return this._member; } }// public string EmployeeNumber { get { return _member.EmployeeNumber; } } public Int32? CardNo { get { return _member.CardNo; } } public string LastName { get { return _member.LastName; } } public string FirstName { get { return _member.FirstName; } } public string Addr1 { get { return _member.Addr1; } } public string Addr2 { get { return _member.Addr2; } } public string City { get { return _member.City; } } public string State { get { return _member.State; } } public string Zip { get { return _member.Zip; } } } |
C# | public string MembersCount { get { if (this.Members.Count == 0) return "No members"; if (this.Members.Count == 1) return "1 member"; return this.Members.Count + " members"; } } public MemberQuery MemberQuery { get { return _memberQuery; } set { _memberQuery = value; OnPropertyChanged("MemberQuery"); } } public MemberViewModel SelectedItem { get { return _selectedItem; } set { if (_selectedItem != value) { if (_selectedItem != null) _selectedItem.Member.PropertyChanged -= Member_PropertyChanged; if (value != null) value.Member.PropertyChanged += Member_PropertyChanged; _selectedItem = value; OnPropertyChanged("SelectedItem"); OnPropertyChanged("SelectionStatus"); InvalidateCommands(); } } } public string SelectionStatus { get { return this.SelectedItem == null ? "No member selected" : "Selected member: " + this.SelectedItem.Member.LastName + ", " + this.SelectedItem.Member.LastName; } } public bool IsBusy { get { return _isBusy; } internal set { if (_isBusy != value) { _isBusy = value; OnPropertyChanged("IsBusy"); } } } |
Next we have a public property that provides access to the QueryDescriptor object _queryDescriptor. Just copy this code as is in each project. Notice that the getter checks to see if an instance of the _queryDescriptor has already been instantiated, otherwise it uses the constructor to create a new one.
C# | public QueryDescriptor QueryDescriptor { get { if (this._queryDescriptor == null) { this.QueryDescriptor = new QueryDescriptor(); } return this._queryDescriptor; } set { if (this._queryDescriptor != value) { if (this._queryDescriptor != null) this._queryDescriptor.QueryChanged -= new System.EventHandler(OnQueryChanged); this._queryDescriptor = value; this._queryDescriptor.QueryChanged += new System.EventHandler(OnQueryChanged); this.OnPropertyChanged("QueryDescriptor"); } } } |
C# | public void BuildFilter() { if (this.QueryDescriptor.FilterDescriptors.Count >= 1) this.QueryDescriptor.FilterDescriptors.Clear(); CompositeFilterDescriptorCollection groupFilter1 = new CompositeFilterDescriptorCollection(); groupFilter1.LogicalOperator = FilterCompositionLogicalOperator.And; if (MemberQuery.SSN != string.Empty) { groupFilter1.Add( new FilterDescriptor() { PropertyName = "SocSecNo", Operator = FilterOperator.StartsWith, Value = MemberQuery.SSN } ); } if (MemberQuery.EmployeeNo != string.Empty) { groupFilter1.Add( new FilterDescriptor() { PropertyName = "EmployeeNumber", Operator = FilterOperator.StartsWith, Value = MemberQuery.EmployeeNo } ); } if (MemberQuery.CardNo != null) { groupFilter1.Add( new FilterDescriptor() { PropertyName = "EmployeeNumber", Operator = FilterOperator.StartsWith, Value = MemberQuery.EmployeeNo } ); } if (MemberQuery.LastName != string.Empty) { groupFilter1.Add( new FilterDescriptor() { PropertyName = "LastName", Operator = FilterOperator.StartsWith, Value = MemberQuery.LastName } ); } if (MemberQuery.FirstName != string.Empty) { groupFilter1.Add( new FilterDescriptor() { PropertyName = "FirstName", Operator = FilterOperator.StartsWith, Value = MemberQuery.FirstName } ); } if (groupFilter1.Count >= 1) _queryDescriptor.FilterDescriptors.Add(groupFilter1); } |
In a series of “if” statements, I check each property of the Member Query object and see if the user typed criteria into that field on the View. Each property of the query object is mapped to a text box on the criteria search panel If the property has a value, I add it to the group filter. The property name is the field domain object being searched, in this case, the member table. I then indicate the type of Operator to use such as “Equals”, “Less than”, Greater than” etc. In the case of LastName and FirstName I use “Starts With” to allow for partial name searches. Finally I assign the property in the query object to the Value property of the FilterDescriptor object.
I check to see if the user entered any criteria at all. If so I add all the various filterOperators to the QueryDescriptor.FilterDescriptors object.
Next we have the BuildSort function. While the user can click on any column to sort the grid after the data is displayed, I would like to have it initially display in a specific order.
C# | public void BuildSortOrder() { _queryDescriptor.SortDescriptors.Add( new SortDescriptor() { PropertyName = "LastName", Direction = System.ComponentModel.ListSortDirection.Ascending } ); _queryDescriptor.SortDescriptors.Add( new SortDescriptor() { PropertyName = "FirstName", Direction = System.ComponentModel.ListSortDirection.Ascending } ); } |
Finally we get have the Fetch method that will call the repository function GetMembers, pass it the QueryDescriptor object and return the rows retrieved.
C# | private void FetchMember() { this.IsBusy = true; Repository.GetMembers(_queryDescriptor, (members) => // query success { this.IsBusy = false; ResetMemberList(members); }, (error) => // failed { this.IsBusy = false; DisplayMessage(error.Message); }); } |
C# | private void ResetMemberList(IEnumerable<Member> member) { this.Members.Clear(); member.ForEach(c => Members.Add(new MemberViewModel(c~)~)~); } |
C# | private void DisplayMessage(string message) { this.MessagePresenter.ShowInformationMessage(message); } |
C# | private void Member_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "LastName" || string.IsNullOrEmpty(e.PropertyName)) OnPropertyChanged("SelectionStatus"); } private void Member_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged("MembersCount"); } |
There are several things that we have not yet covered in the view model class but they will be covered in section 3. The source code will have the complete code from all the classes seen up to this point.
In section 2, we covered the creation of the view model class. Remember the View Model class handles all the commands from the View class and holds the state of the View class. In the next section we will see how commands are sent from the view to the view model and how the view is notified of any changes to the state.
In sections 1 and 2, we looked at how to create the view and view model classes. In this section, we will see how the View talks to the View Model class and vice versa. In MVVM, we want to couple the two classes together as loosely as possible. I may in the future want to use a different view with my view model and do not want to have to tear a lot of code out in order to do that. I would like to just create a new view and maybe add some properties to my view model but that is it.
In this section I am just going to show the appropriate code from the View and View Model as it pertains to sending commands from the view to view model. I will not explain code and xaml that was discussed in section 1.
In the View class, I need to tell it which ViewModel it is going to send it is associated with. Just above the opening Grid tag, I will place this piece of code.
Intersoft | <Intersoft:UXPage.DataContext> <ViewModels:MembersListViewModel /> </Intersoft:UXPage.DataContext> |
This tells the View that the DataContext or information to be displayed in the objects on the View come from the MembersListViewModel in the ViewModels folder. Just below my opening Grid tag I will set up the bindings for the various commands on the toolbar.
Intersoft | <Grid.Resources> <Intersoft:CommandReference x:Key="AddCommand" Command="{Binding AddCommand}"/> <Intersoft:CommandReference x:Key="EditCommand" Command="{Binding EditCommand}"/> <Intersoft:CommandReference x:Key="ViewCommand" Command="{Binding ViewCommand}"/> <Intersoft:CommandReference x:Key="DeleteCommand" Command="{Binding DeleteCommand}"/> <Intersoft:CommandReference x:Key="FindCommand" Command="{Binding FindCommand}"/> <Intersoft:CommandReference x:Key="ShowReportsCommand" Command="{Binding ShowReportsCommand}"/> </Grid.Resources> <Intersoft:CommandManager.InputBindings> <Intersoft:InputBindingCollection> <Intersoft:KeyBinding Command="{StaticResource AddCommand}" Gesture="Ctrl+Shift+A"/> <Intersoft:KeyBinding Command="{StaticResource EditCommand}" Gesture="Ctrl+Shift+E"/> <Intersoft:KeyBinding Command="{StaticResource ViewCommand}" Gesture="Ctrl+Shift+V"/> <Intersoft:KeyBinding Command="{StaticResource DeleteCommand}" Gesture="Ctrl+Shift+X"/> <Intersoft:KeyBinding Command="{StaticResource FindCommand}" Gesture="Ctrl+Shift+F"/> <Intersoft:KeyBinding Command="{StaticResource ShowReportsCommand}" Gesture="Ctrl+Shift+R"/> </Intersoft:InputBindingCollection> </Intersoft:CommandManager.InputBindings> |
In the InputBindings tag, I can indicate special key is used to access the buttons . For example, Ctrl+Shift+F is used for the Find command. This is not a necessary section.
Then within the xaml that sets up the toolbar I bind the command property of each button to one of the above commands.
Intersoft | <Intersoft:UXToolGroup MinWidth="450"> <Intersoft:UXToolBarButton Command="{Binding AddCommand}" Content="Add New Member" ToolTipService.ToolTip="Add a new member (Ctrl + Shift + A)"/> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding EditCommand}" Content="Edit Member" ToolTipService.ToolTip="Edit selected member (Ctrl + Shift + E)" /> <Intersoft:UXToolBarButton Command="{Binding EditCommand}" Content="View Member" ToolTipService.ToolTip="View selected member (Ctrl + Shift + V)" /> <Intersoft:UXToolBarButton Command="{Binding DeleteCommand}" Content="Delete Member" ToolTipService.ToolTip="Delete selected member (Ctrl + Shift + X)"/> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding FindCommand}" Content="Find Member" ToolTipService.ToolTip="Find member" /> <Intersoft:UXSeparator/> <Intersoft:UXToolBarButton Command="{Binding ShowReportsCommand}" Content="Member Reports" ToolTipService.ToolTip="" /> </Intersoft:UXToolGroup> </Intersoft:UXToolBar> |
As far as the grid goes we have already seen the ItemsSource property, the SelectedItem property, and the column definitions, so I won’t go through those again. If you have forgotten them or wish to refresh your memory go back to section 1.
In the View Model class, I set up the various command variables.
C# | public DelegateCommand AddCommand { get; private set; } public DelegateCommand EditCommand { get; private set; } public DelegateCommand DeleteCommand { get; private set; } public DelegateCommand FindCommand { get; private set; } public DelegateCommand ShowReportsCommand { get; private set; } |
And in the constructor for the class, I instantiate each one.
C# | this.AddCommand = new DelegateCommand(AddMember, CanAddMember); this.EditCommand = new DelegateCommand(EditMember, CanEditMember); this.DeleteCommand = new DelegateCommand(DeleteMember, CanDeleteMember); this.FindCommand = new DelegateCommand(FindMember, CanFindMember); this.ShowReportsCommand = new DelegateCommand(ShowReports, CanShowReports); |
Notice that we use the DelegateCommand class to create the command objects and the constructor takes two parameters the first parameter is the method to call when the command is triggered on the View. The second parameter is the method that is checked continuously to see if the button or simply the command should be allowed to occur. For example, if the grid has no rows in it, it would be useless the user to click on the “Edit” or “Delete” button. If they do I have to check in my code to make sure the user is situated on a valid row in the grid. Instead, I will disable the buttons if no row is selected. In addition,some of the users may not have permission to delete rows from tables. I can check for security permissions in the CanDelete() method and enable or disable the appropriately.
Let’s just look at the Find Command section for the purposes of this tutorial.
C# | private bool CanFindMember(object parameter) { return true; } private void FindMember(object parameter) { BuildFilter(); if (this.QueryDescriptor.FilterDescriptors.Count >= 1) { BuildSortOrder(); FetchMember(); } else { DisplayMessage("Some criteria must be selected before searching records."); } } |
I can’t think of any reason why a user should not be able to find a record, so I always return true to enable the button at all times. I could check to see if any of the fields are filled in on the search panel and enable or disable the button, but I will leave that task for the reader to do.
In the FindMember method, I call the BuildFilter() method covered in the last section. I check to see if any filters have been set. Because a member table might have thousands upon thousands of records in them and this is a Silverlight application with data flowing back and forth over the internet, I want to ensure that at least one criteria field was filled in to attempt to limit the returned number of rows. If so, I call the BuildSortOrder and FetchMember methods as covered in the section 2. If no criteria was selected, I display a message to the user notifying them of that fact.
Finally we have the InvalidateCommand. This method is called by the SelectedItem property and indicates that the selection has changed and the Can methods should be rechecked to see if any of the buttons should be enabled or disabled.
C# | private void InvalidateCommands() { // Invalidate the commands that related to this view scope// this.AddCommand.RaiseCanExecuteChanged(); this.EditCommand.RaiseCanExecuteChanged(); this.DeleteCommand.RaiseCanExecuteChanged(); this.FindCommand.RaiseCanExecuteChanged(); this.ShowReportsCommand.RaiseCanExecuteChanged(); } |
Well that covers how the commands are sent from the view to the view model class and how they are processed by the view model and state is returned to the view. In the next section we will show how to create the repository.
In the first three sections of this tutorial, we built the view, the view model, and we did it all using the MVVM framework. The view contains only the objects the user will see and interact with. The view could have been developed by a designer using a tool such as Microsoft Expression Blend 4. The view model contains all the commands and state of certain properties and fields. The view model could have been separately created by a programmer at the same time the view created by a designer.
In this section we will add a repository class to access the data source. By keeping the data access to a separate class, if in the future we switch to a different entity framework, it will easy to update the program. There are actually several classes we will create in this section. First let’s take a quick review at how the repository is accessed by the view model.
In the view model class, we declare an object to represent the repository and instantiate the object in the constructor either to an instance of an existing repository or a new one is created.
C# | public ITicketWareRepository Repository { get; set; } this.Repository = TicketWareRepository.Instance; |
In the Model Services folder, let’s add a new class and name it TicketWareRepository. We will have it extend from the interface, ITicketWareRepository. For the purposes of this tutorial the ITicketRepository is not used. Create a local variable for the repository and a public property for accessing it.
C# | private static ITicketWareRepository _repository; internal TicketWareEntities Manager { get; private set; } |
C# | public static ITicketWareRepository Instance { get { return _repository ?? (_repository = CreateRepository()); } set { _repository = value; } } public TicketWareRepository(TicketWareEntities entityManager) { Manager = entityManager; } |
And here is the CreateRepository method.
C# | public static ITicketWareRepository CreateRepository() { return new TicketWareRepository(EntityManagerFactory.Create()); } Note that the CreateRepository method calls the EntityManagerFactory class public static class EntityManagerFactory { public static TicketWareEntities Create() { var manager = new TicketWareEntities(); return manager; } } |
While the repository contains all the create methods, the delete methods, the save, cancel, and Get methods, all that we care about in this tutorial is the GetMembers method that we saw called from the FetchMembers of the view model class in section 2.
C# | public void GetMembers(QueryDescriptor queryDescriptor, Action<IEnumerable<Member>> onSuccess, Action<Exception> onFail) { Manager.Members.Parse(queryDescriptor) .ExecuteAsync( op => { if (op.CompletedSuccessfully) { if (onSuccess != null) onSuccess(op.Results); } else { if (onFail != null) { op.MarkErrorAsHandled(); onFail(op.Error); } } } ); } |
So let’s review. In this tutorial, we learned how to create a view that had a search criteria panel and a grid panel. We saw how to send commands from the view to the view model and how to access the state of properties and fields to refresh the objects on the view. The entire tutorial was built using the View Model View-Model framework using just Intersoft ClientUI and IdeaBlade DevForce for Silverlight. The View contains all the objects the user interacts with on the screen. The View Model class contains all the commands, the state of the variables, and calls to the repository. Finally the repository calls the entity framework to access the data source.
While it may seem like it took a lot of work to get to this point, it is amazing how much background work both ClientUI and Intersoft have done for us and have eased our development time. Without them, we would have needed additional frameworks in order to accomplish the MVVM that was built into the UI controls themselves. DevForce from the moment it is used to create the entity framework to the calls we use to access the data, is doing a lot of work in the background. Without a tool like DevForce, our job as a programmer would be much harder.
Creating this part from scratch took me only a couple of hours. Now there is a lot more functionality to add in and many more views and view Models and repository methods to add to make this a sellable project, but I hope that you have seen how easy it is to begin an MVVM application using Intersoft ClientUI 5 and IdeaBlade DevForce for Silverlight.
I am not an employee of either Intersoft or IdeaBlade. I am an independent developer who uses these tools on a daily basis to be more productive with my time.
-William H. Gower