An MVVM Approach to Telerik Domain Services for Silverlight

by XAML Team | Comments 22

There are only about 7 hours we have left in 2010 and this will be my last blog post for this year. For those of you who are not familiar with the new RadDomainDataSource control for Silverlight, here is my introductory blog post. This one describes how to load data with the new control and this one is about performing CRUD. Having read these three blogs might lead you to the next logical question: What about MVVM support?

I truly believe that every time someone places an UI element or a control in his view model, a baby kitten dies somewhere. So what can we do about this?MVVM

Luckily, the class that RadDomainDataSource internally uses as its view is public and can be used directly in your view models. You can learn about the relationship between the RadDomainDataSource control and the QueryableDomainServiceCollectionView here.

The architecture of my sample project is fairly simple. I have a page with a grid, a pager and a bunch of other configuration controls. All of these UI elements are bound to a common view model which is the data context of the root element:

  1. <UserControl x:Class="SilverlightApplication1.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6.     xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
  7.     xmlns:e="clr-namespace:SilverlightApplication1.Web.Services"
  8.     xmlns:my="clr-namespace:SilverlightApplication1"
  9.     mc:Ignorable="d"
  10.     d:DesignHeight="300" d:DesignWidth="400">
  11.     <UserControl.Resources>
  12.         <my:CustomersViewModel x:Key="CustomersViewModel"/>
  13.     </UserControl.Resources>
  14.     <Grid DataContext="{StaticResource CustomersViewModel}">
  15.         <Grid.ColumnDefinitions>
  16.             <ColumnDefinition Width="*"/>
  17.             <ColumnDefinition Width="250"/>
  18.         </Grid.ColumnDefinitions>
  19.         <Grid Name="mainGrid" Grid.Column="0">
  20.             <Grid.RowDefinitions>
  21.                 <RowDefinition Height="*" />
  22.                 <RowDefinition Height="Auto" />
  23.             </Grid.RowDefinitions>
  24.             <telerik:RadGridView
  25.                 x:Name="radGridView"
  26.                 Grid.Row="0"
  27.                 ItemsSource="{Binding View}"
  28.                 IsBusy="{Binding IsBusy}"
  29.                 ShowGroupPanel="False"
  30.                 RowIndicatorVisibility="Collapsed"
  31.                 IsFilteringAllowed="False"
  32.                 CanUserSortColumns="False"
  33.                 AutoGenerateColumns="False"
  34.                 IsReadOnly="True">
  35.                 <telerik:RadGridView.Columns>
  36.                     <telerik:GridViewDataColumn DataMemberBinding="{Binding Country}"/>
  37.                     <telerik:GridViewDataColumn DataMemberBinding="{Binding City}"/>
  38.                     <telerik:GridViewDataColumn DataMemberBinding="{Binding ContactName}" Header="Contact"/>
  39.                     <telerik:GridViewDataColumn DataMemberBinding="{Binding ContactTitle}" Header="Title"/>
  40.                     <telerik:GridViewDataColumn DataMemberBinding="{Binding CompanyName}" Header="Company"/>
  41.                 </telerik:RadGridView.Columns>
  42.             </telerik:RadGridView>
  43.             <telerik:RadDataPager x:Name="radDataPager"
  44.                                   Grid.Row="1"
  45.                                   Margin="0, 0, 0, 1"
  46.                                  Source="{Binding View}"
  47.                                   DisplayMode="All"
  48.                                  IsTotalItemCountFixed="True"/>
  49.         </Grid>
  50.         <StackPanel Grid.Column="1" Margin="3">
  51.  
  52.             <Grid>
  53.                 <Grid.RowDefinitions>
  54.                     <RowDefinition/>
  55.                     <RowDefinition/>
  56.                 </Grid.RowDefinitions>
  57.                 <Grid.ColumnDefinitions>
  58.                     <ColumnDefinition/>
  59.                     <ColumnDefinition/>
  60.                 </Grid.ColumnDefinitions>
  61.                 <CheckBox Content="Auto"
  62.                           Margin="1"
  63.                          VerticalAlignment="Center"
  64.                          VerticalContentAlignment="Center"
  65.                           IsChecked="{Binding AutoLoad, Mode=TwoWay}"/>
  66.                 <telerik:RadButton Grid.Column="1"
  67.                                    Content="Refresh"
  68.                                    Margin="1"
  69.                                    Padding="10,2"
  70.                                    Command="{Binding LoadCommand}"/>
  71.                 <TextBlock Grid.Row="1"
  72.                            Text="Page Size"
  73.                            VerticalAlignment="Center"
  74.                            Margin="1" />
  75.                 <telerik:RadNumericUpDown Grid.Row="1"
  76.                                           Grid.Column="1"
  77.                                           Margin="1"
  78.                                          Value="{Binding PageSize, Mode=TwoWay}"
  79.                                          Minimum="0"
  80.                                           IsInteger="True"/>
  81.             </Grid>
  82.  
  83.             <TextBlock Text="Sort by" Margin="0,10,0,0" FontWeight="Bold" />
  84.             <TextBlock Text="Country" />
  85.             <RadioButton Content="Ascending"
  86.                          GroupName="CountrySort"
  87.                          IsChecked="{Binding IsCountryAscendingChecked, Mode=TwoWay}"/>
  88.             <RadioButton Content="Descending"
  89.                          GroupName="CountrySort"
  90.                          IsChecked="{Binding IsCountryDescendingChecked, Mode=TwoWay}"/>
  91.             <RadioButton Content="None"
  92.                          GroupName="CountrySort"
  93.                          IsChecked="{Binding IsCountryNoneChecked, Mode=TwoWay}"/>
  94.             <TextBlock Text="Then by" Margin="0,10,0,0" FontWeight="Bold" />
  95.             <TextBlock Text="City" />
  96.             <RadioButton Content="Ascending"
  97.                          GroupName="CitySort"
  98.                          IsChecked="{Binding IsCityAscendingChecked, Mode=TwoWay}"/>
  99.             <RadioButton Content="Descending"
  100.                          GroupName="CitySort"
  101.                          IsChecked="{Binding IsCityDescendingChecked, Mode=TwoWay}"/>
  102.             <RadioButton Content="None"
  103.                          GroupName="CitySort"
  104.                          IsChecked="{Binding IsCityNoneChecked, Mode=TwoWay}"/>
  105.  
  106.             <TextBlock Text="Filter by" Margin="0,10,0,0" FontWeight="Bold" />
  107.             <TextBlock Text="Contact Title" Margin="0,5,0,0" />
  108.             <telerik:RadComboBox ItemsSource="{Binding ContactTitles}"
  109.                                  SelectedValue="{Binding SelectedContactTitle, Mode=TwoWay}"/>
  110.         </StackPanel>
  111.     </Grid>
  112. </UserControl>

 

All of the business logic happens inside this view model, which makes it perfect for unit testing. A central part of the view model is an instance of the QueryableDomainServiceCollectionView. The view itself and several of its properties are exposed as properties of the view model, so various UI elements can be bound to them:

  1. private readonly QueryableDomainServiceCollectionView<Customer> view;
  2.  
  3. public CustomersViewModel()
  4. {
  5.     NorthwindDomainContext context = new NorthwindDomainContext();
  6.     EntityQuery<Customer> getCustomersQuery = context.GetCustomersQuery();
  7.     this.view = new QueryableDomainServiceCollectionView<Customer>(context, getCustomersQuery);
  8.     //...
  9. }

 

To illustrate this, let’s take a look at how the page size can be changed by the user. The view contains a RadNumericUpDown bound to the PageSize property of the view model:

  1. <telerik:RadNumericUpDown Grid.Row="1"
  2.                           Grid.Column="1"
  3.                           Margin="1"
  4.                          Value="{Binding PageSize, Mode=TwoWay}"
  5.                          Minimum="0"
  6.                           IsInteger="True"/>

 

The view model simply forwards the page changing logic between the view and the model. In my case there is no additional complex logic involved, but I your real-life projects may be more complex than this:

  1. public int PageSize
  2. {
  3.     get
  4.     {
  5.         return this.view.PageSize;
  6.     }
  7.     set
  8.     {
  9.         if (this.view.PageSize != value)
  10.         {
  11.             this.view.PageSize = value;
  12.             this.OnPropertyChanged("PageSize");
  13.         }
  14.     }
  15. }

 

The view model gives you unlimited opportunities to inject custom logic in both directions. As a matter of fact, RadDomainDataSource is built in a very similar way. It aggregates an instance of the QueryableDomainServiceCollectionView and simply forwards information forth and back. So, for example, when you go and change RadDomainDataSource.PageSize to 10, it will simply change the PageSize of its view to 10. In fact, you can write these two lines of code and they will do absolutely the same thing:

  1. radDomainDataSource.PageSize = 10;
  2. radDomainDataSource.DataView.PageSize = 10;

 

You can totally bypass RadDomainDataSource and work directly with the view. That’s the reason why in my previous blog post I have described RadDomainDataSource as just a “XAML-friendly thin wrapper over the QDSCV”. Come to think of it, when you are developing your very own view models that will aggregate a QDSCV, you are kind of writing your very own personal RadDomainDataSource that does exactly what you need. With the only difference that RadDomainDataSource is a control and your view model is not.

187x127_ChristmasCardIn the sample project that I have attached you will also see how to use buttons with commands, how to listen for changes in the QDSCV, and how to retrieve and expose distinct values. So attach your debuggers and hit F5.

May your source code compile and your unit tests pass in 2011! I wish you a Happy New Year!

Download Sample Project


Senior Software Developer,
Telerik XAML Team

22 Comments

Ben
Thanks for honoring your word my friend. It would have been hard to wait till NEXT YEAR :-)

Have great new year
..Ben
Ben
Rossen, so if we go by eliminating the use of DS in the XAML and work with your collection, does it mean the RADGridView is no longer offering the automatic capability for user to click on columns to do sorting, even though in XAML I set CanUserSortColumns="True". And we have to manually handle the sorting for each form as you have shown it?
Thanks!
Ben
One other question regarding the availability of this library as of now until the next SP.
I see you have a Lib folder with a series of Libraries from Telerik and at the same time in your references, you are referencing a series of Telerik Libraries.
Are you using these NEW (pre SP) in the Lib folder for the time being until the final release? Do you suggest to create a folder like this and in our "Using" point to this folder and when the SP is released, point back to the references section?
Just want to make sure how we can use it and switch over properly.
Thanks!
Rossen Hristov
Hello Ben,

1. If you bind RadGridView.ItemsSource to a QueryableDomainServiceCollectionView they will synchronize sorting and filtering automatically. You don't have to handle anything manually, you can try this. That is because the QDSCV implements the IQueryableCollectionView interface. Every class that implements the IQueryableCollectionView interface will sync its descriptors with RadGridView out-of-the-box.

2. The assemblies in my sample projects are from the latest internal build and they are publicly available. Nothing will change until the official SP comes out, i.e. the domain services assembly is the same as the one that you will get with the SP, so you can start using it right away. The thing is that we wanted to launch this new control with the SP in mid-December. Since the SP got delayed for other reasons, we decided to let people know about this control so they can start experimenting with it. In other words, the control is feature complete in its current version, i.e. the one you see in my sample projects.
janoveh
Hi!
Will there be something like "VirtualQueryableDomainServiceCollectionView" for scenarios where data virtualization is desired with a WCF RIA Services data source in a MVVM-based solution?
Or is there an option to somehow combine "VirtualQueryableCollectionView" and "QueryableDomainServiceCollectionView"?
 Thanks!
Ben
@Janoveh;
Good question, but I think the virtualization engine is built into this collection. But I'll wait for the team to respond to this question.
Rossen Hristov
Hello janoveh,

Please, take a look at this blog post:

http://blogs.telerik.com/vladimirenchev/posts/10-12-09/server_sorting_and_filtering_with_wcf_ria_services_and_telerik_data_virtualization_for_silverlight.aspx
James
I am curious about the overall performance impact of this approach vs. a pure WCF RIA Services DataGrid. 
Rossen Hristov
The overall performance should be the same. However, I am not sure how you measure performance.

In client-server scenarios like this the bottleneck will almost always be be the round-trip to the server, and since this is really handled by WCF RIA Services, I guess that the overall performance will be the same. But you can run some tests if you want. The only difference will be on the client.
Leos
> If you bind RadGridView.ItemsSource to a QueryableDomainServiceCollectionView they will synchronize sorting and filtering automatically.

Yes, this is working, but to see these changes must be QDSCV explicitly reloaded. Does exist some easy way how to reload QDSCV every time when (and only when) sorting/filtering (by user click on RadGridView header) is changed?
Rossen Hristov
Hello Leos,

You can use the AutoLoad property.
Leos
Thank you, it is working fine.
Leos
Dale Durham

Hi Rossen,

Are there any inherent capabilities to overcome some of the other MVVM specific challenges such as commanding and actions/behaviours? If not, they can be handled the way the are now (code), but I am just curious if this exists or there are plans for it.

 

Thanks,

Dale

 

Dale Durham
Never mind - I should look BEFORE I leap. I see that there is at least COMMANDING support. However, it does not look like there is design time support - at least not yet. The view model throws an exception that it can't be created when you open the xaml in the designer. Obviously this is because it cannot call the service, so you may want to consider modifying the view model to support design time "mock" data.

Otherwise - VERY cool stuff!

Dale
Rossen Hristov
Hello Dale,

This is just an example of a possible view model. This is one possible view model out of an infinite number of possible view models. Every developer will create a view model that will suit his or her particular needs.

In other words, the view model code in the sample project is not a product of its own. It is just an example that can be built upon. I am sure that you can easily add design-time support to your own view model in case you need it.

Jerry T.
Is this sample project not setup to be an editable grid?
Also, the downloaded project fails for me in GetDistinctValues at:


foreach
(var value in values)

with the error:
"The underlying provider failed on Open."
Almond
I would like to ask if we can have a search as you type functionality in the above sample of MVVM.
I had successfully implemented the above code, however am not able to successfully add the search as you type functionality.  Most of the time the result of the filtering is not what was expected.  
here is the code which i inserted in the view model declaration:
        public ContractViewModel()
        {
            var myServiceContractContext = new csisDomainContext();
            
            EntityQuery<v_contactfullname> getVContactFullNamePartialPaymentQuery = myServiceContractContext.GetV_ContactFullNamePartialPaymentQuery();
            _view = new QueryableDomainServiceCollectionView<v_contactfullname>(myServiceContractContext, getVContactFullNamePartialPaymentQuery)
                             {PageSize = 6, AutoLoad = true};
                     _view.PropertyChanged += OnViewPropertyChanged;
            _loadCommand = new DelegateCommand(ExecuteLoadCommand, LoadCommandCanExecute);            
            _mainFilterDescriptor.LogicalOperator = FilterCompositionLogicalOperator.Or;
            var crcnoFd = new FilterDescriptor("service_crcno", FilterOperator.Contains, OperatorValueFilterDescriptorBase.UnsetValue);
            _mainFilterDescriptor.FilterDescriptors.Add(crcnoFd);
            var branchFd = new FilterDescriptor("branch_code", FilterOperator.Contains, OperatorValueFilterDescriptorBase.UnsetValue);
            _mainFilterDescriptor.FilterDescriptors.Add(branchFd);
            var lnameFd = new FilterDescriptor("service_decfullname", FilterOperator.Contains, OperatorValueFilterDescriptorBase.UnsetValue);
            _mainFilterDescriptor.FilterDescriptors.Add(lnameFd);
            var typeFd = new FilterDescriptor("service_type", FilterOperator.Contains, OperatorValueFilterDescriptorBase.UnsetValue);
            _mainFilterDescriptor.FilterDescriptors.Add(typeFd);
            _view.FilterDescriptors.Add(_mainFilterDescriptor);               
        }
        public string SearchContractString
        {
            get
            {
                if (_mainFilterDescriptor.FilterDescriptors.Cast<FilterDescriptor>().Any(descriptor => descriptor.Value == OperatorValueFilterDescriptorBase.UnsetValue))
                {
                    return ClearSelectionString;
                }
                return ClearSelectionString;
            }
            set
            {
                foreach (FilterDescriptor descriptor in _mainFilterDescriptor.FilterDescriptors)
                {
                    descriptor.Value = value != ClearSelectionString ? value : OperatorValueFilterDescriptorBase.UnsetValue;
                }                
            }
        }
as you can see i added multiple number of filter descriptors and then created a SearchContractString where in i bind my TextBox as seen in the code below:
                            <telerik:RadMaskedTextBox x:Name="locationText" MaskType="None" VerticalAlignment="Center" HorizontalAlignment="Left" Width="200" EmptyContent="" UpdateValueEvent="PropertyChanged" SelectionOnFocus="Default" 
                                                              Style="{StaticResource childWindowTextBox}" TabIndex="1" 
                                                              Loaded="locationText_Loaded" ValueChanged="locationText_ValueChanged" Margin="4" />


I think there is something wrong with my SearchContractString declaration of the get and set properties.  I do really hope someone can point me out on how to fix my problem.  To be honest i have been looking for the solution for weeks now.
Thanks in advance Telerik Gurus.
Rob
"1. If you bind RadGridView.ItemsSource to a QueryableDomainServiceCollectionView they will synchronize sorting and filtering automatically."

At the moment this doesn't seem to be working from me. In case anyone is home, any ideas why?
Josh
Same.. Not working.  The grid is not updating, but the view has been.

Whats going on?
Josh
Same.. Not working.  The grid is not updating, but the view has been.

Whats going on?
Josh
Same.. Not working.  The grid is not updating, but the view has been.

Whats going on?

Comments

  1.    
      
      
       
  2. (optional, emails won't be shown on public pages)
  3. (optional)
Read more articles by XAML Team - or - read latest articles in Developer Tools