DomainCollectionView Updates for Mix ‘11


To correspond to the new RIA Services build we’ve released at Mix, I’ve made some updates to the DomainCollectionView. I updated select API, fixed a few bugs, and updated the sample as well. If you’re not familiar with the DCV, here’s my original post introducing it. For the sake of brevity in this post, I’ll assume you’ve read it.

Breaking Changes

There are a few breaking changes to the API. Most notably, SortPageAndCount has been renamed. It’s a breaking change, but a simple find-replace should set things right again.

  • SortAndPageBy replaces SortPageAndCount
    • SortAndPageBy calls SortBy and PageBy respectively
  • SortBy replaces Sort
    • SortBy applies OrderBy and ThenBy clauses to the query
  • PageBy replaces Page
    • PageBy applies Skip and Take clauses to the query and conditionally requests the TotalEntityCount
  • (All the other query extensions have been removed)

Bug Fixes

There were a few bug fixes that went in to improve compatibility with third-party controls. I’ve tested the DCV against four suites of controls now, and it works well with all of them. As always, let me know when you find issues.

Sample

A common question in response to the first post was how to implement filtering. It turns out to be pretty easy, so I wanted to make it obvious with some updates to my sample.

I’ve updated the UI to include a search field.

image 

The search button now invokes the search command which calls into the OnSearch method in my SampleViewModel.

  private void OnSearch()
  {
    // This makes sure we refresh even if we're already on the first page
    using (this._view.DeferRefresh())
    {
      // This will lead us to re-query for the total count
      this._view.SetTotalItemCount(-1);
      this._view.MoveToFirstPage();
    }
  }

The OnSearch method resets the total item count and refreshes the view; leading us back into our load callback.

  private LoadOperation<SampleEntity> LoadSampleEntities()
  {
    this.CanLoad = false;

    EntityQuery<SampleEntity> query = this._context.GetAllEntitiesQuery();
    if (!string.IsNullOrWhiteSpace(this.SearchText))
    {
      query = query.Where(e => e.String.Contains(this.SearchText));
    }

    return this._context.Load(query.SortAndPageBy(this._view));
  }

To make sure the filter gets applied on the server, we conditionally add a Where clause to our query based on the content of the SearchText.

That covers the new additions to the DomainCollectionView. Once again we’ve put together a small sample running with server-side filtering, sorting, grouping, and paging. Here’re the sample bits. Let me know if you have any questions.

http://code.msdn.microsoft.com/Server-Side-Filtering-737becda

Comments (25)

  1. Great stuff! I have installed and once I altered the code for breaking changes all appears to work great!

    I am trying to implement page level caching to avoid loading pages more than once if they have already been loaded.  I clear the cache anytime I save out changes where I have either added/removed enitties.

    I have create a very simple page cache generic type that stores the page results (total Entity count & the IEnumerable<T> results for a given page load. (see below).

    Logically I works well, however the page count gets messed up. I suspect its something to do with the fact I am returning a null when I resort to the page cache in the LoadOperation method.  

    Is there any good guidance on how to implement page cache ? Is there something you see as an obvious miss in my code below?

    As always I appreciate the effort and any assistance!  Thank you in advance.

    ……..

    Here are the page cache types I came up with..

       public class PageQueryResult<T>

       {

           public int TotalEntityCount { get; set; }

           public IEnumerable<T> QueryResults { get; set; }

       }

       public class PageCache<T>

       {

           private Dictionary<int,PageQueryResult<T>> _pagesCache;

           public bool IsPageLoaded(int page)

           {

               if (_pagesCache == null)

                   _pagesCache = new Dictionary<int,PageQueryResult<T>>();

               return _pagesCache.ContainsKey(page);

           }

           public void Add(int page, int totalEntityCount, IEnumerable<T> queryResults)

           {

               if (IsPageLoaded(page))

               {

                   _pagesCache.Remove(page);

               }

               _pagesCache.Add(page,new PageQueryResult<T> {TotalEntityCount= totalEntityCount, QueryResults = queryResults});

           }

           public PageQueryResult<T> GetPage(int page)

           {

               return !IsPageLoaded(page) ? null : _pagesCache[page];

           }

           public void Clear()

           {

               if (_pagesCache == null)

                   _pagesCache = new Dictionary<int, PageQueryResult<T>>();

               _pagesCache.Clear();

           }

       }

    … and here is an implementation that attempts to leverage this to cache the pages..

    // Used to track and cache page results for TaskLog

     private readonly PageCache<TaskLog>  _taskPagesLoadedCache = new PageCache<TaskLog>();

     public LoadOperation<TaskLog> LoadTaskLogs()

           {

                   // Check if the task page has already queried

                   if (!_taskPagesLoadedCache.IsPageLoaded(_view.PageIndex))

                   {

                             …  create query …

                            var op =  Context.Load(qry);

                           return op;

                  }

                   // This page is cached so simply setup the source and view counts with the cached results

                   _source.Source = _taskPagesLoadedCache.GetPage(_view.PageIndex).QueryResults;

                   if (_taskPagesLoadedCache.GetPage(_view.PageIndex).TotalEntityCount != -1)

                   {

                       _view.SetTotalItemCount(_taskPagesLoadedCache.GetPage(_view.PageIndex).TotalEntityCount);

                   }

                   return null; // Not sure what to return ???

           }

    …. and here is the return from the LoadOperation that hit the server …

    public void OnLoadTaskLogsCompleted(LoadOperation<TaskLog> op)

           {

                if (op.HasError)

               {

                   // TODO: handle errors

                   op.MarkErrorAsHandled();

                   _view.SetTotalItemCount(0);

               }

               else if (!op.IsCanceled)

               {

                   _source.Source = op.Entities;

                   if (op.TotalEntityCount != -1)

                   {

                       _view.SetTotalItemCount(op.TotalEntityCount);

                   }

                   // Add this to the cache

                   _taskPagesLoadedCache.Add(_view.PageIndex, op.TotalEntityCount, op.Entities);

                }

           }

    …. and here is the where I test if I need to invalidate the cache …

           public void SaveChanges()

           {

               // Check if the changes we are persisting have added or removed TaskLog records

               // if so, we need to invalidate the Pages Cache

               var changes = BusinessTrackerDs.Context.EntityContainer.GetChanges();

                foreach (var changeset in

                    changes.AddedEntities.Where(changeset => changeset.GetType().Equals(typeof (TaskLog))))

               {

                   _taskPagesLoadedCache.Clear();

               }

                foreach (var changeset in

                    changes.RemovedEntities.Where(changeset => changeset.GetType().Equals(typeof (TaskLog))))

               {

                   _taskPagesLoadedCache.Clear();

               }

               Context.SaveChangesAsync();

           }

  2. kylemc says:

    @Johnny

    For this scenario, I intended you to write a custom CollectionViewLoader. It's a pretty easy class to write and it should feel a bit less hacky if you do it that way. Instead of using the default DomainCollectionViewLoader, just substitute your own.

  3. Thanks Kyle and I do agree my approach is a bit hacky, it was only meant to get a starting soluton for a project I am working on, I do wish to take the proper approach.

    I would very much like to continue to work through your recommended approach. Is there any guidance available on creating a custom CollectionViewLoader.  If not, could you post a psuedo sample it would really help a lot and thanks again, you are the King!

  4. kylemc says:

    @Johnny

    Use the 'Email Blog Author' link above and I can send you a sample.

  5. duluca says:

    Kyle,

    Even though I use SortAndPageBy, I get an error: The method 'Skip' is only supported for sorted input in LINQ to Entities. The method 'OrderBy' must be called before the method 'Skip'

    If I manually click on a column to order things, then it start working.

    I didn't see any sort descriptors in your sample, so I'm a bit at a loss. Any ideas why this could be the case?

  6. kylemc says:

    @duluca

    To use paging with Entity Framework, you need to apply a sort. Our standard recommendation in this scenario is to add a default sort in your DomainService. Something like this.

    public IQueryable<MyEntity> GetMyEntities()

    {

     return this.ObjectContext.MyEntities.OrderBy(e => e.Id);

    }

  7. John says:

    Hi Kyle,

    Likely are really dumb question (at least I really hope it is) but I have multiple scenarios where I bind a dataform to the ICollectionView of a backing DCV view such as here …

     public ICollectionView RegionCollectionView

           {

               get { return RegionDcv; }

           }

    … and via the dataform I am able to edit any of the entities properties exposed within the dataform (works fantastic ) however whenever I have attempted to create a new entity through the AddNew method of the DCV, it creates a new Entity without issue and it does show on the related DataForm but the DataForm is not in an Edit mode. I have to edit some field within the form to get the Submit button to become enabled.  

    Is it possible to make data form enter into an Add and thus have the submit button become enabled through the DCV or do I need to send a message to the view to do that (which  is what I have been doing but it seems a bit odd to me that the form can't detect this automatically)

    Here is and example of my code, where through AddNewRegion method within my ViewModel I create an new Entity on my DCV…

    // Note: DCV is made public to expose editing state to Converter(s)

    public DomainCollectionView<Region> RegionDcv { get; private set; }

    private PageCachingDomainCollectionViewLoader<Region> _regionLoader;

    private EntityList<Region> _regionSource;

    … I have multiple DCV's used within a TreeView (which is not an issue) to represent an small Entity graph of related Entity types, so please ignore the Node stuff …

    // Add's a new Region Entity to the Organization's TreeView and backing DCV

    private void AddNewRegion(HierarchicalViewModel<TreeContainerItem> regionLevelNode)

           {

               if (RegionDcv.IsEditingItem && RegionDcv.CanCancelEdit)

                   RegionDcv.CancelEdit();

               if (!RegionDcv.IsAddingNew)

               {

                   var region = RegionDcv.AddNew() as Region;

                   if (region != null)

                   {

                       region.Description = "New Region";

                       var itemTreeNode = new TreeContainerItem

                                              {

                                                  EntityNode = region,

                                                  LoadBehavior = LoadBehavior.RefreshCurrent,

                                                  TreeNodeFieldName = "Description",

                                                  ParentNode = regionLevelNode,

                                                  Query = BusinessTrackerDs.Context.GetDepartmentsQuery().Where(dept => (dept.RegionID == region.ID) && dept.StatusID == 1),

                                                  CallBack = OnLoadDepartmentResults,

                                                  TreeNodeUserIdentifier = OrganizationalNodeLevel.Region

                                              };

                       var treeContainerItem = new HierarchicalViewModel<TreeContainerItem>(itemTreeNode);

                       _regionSource.Add((Region)(treeContainerItem.PayLoad.EntityNode));

                       regionLevelNode.Children.Add(treeContainerItem); // creates a region tree item

                       SetCurrentPosition(treeContainerItem);

                   }

               }

           }

    … below is the DataForm who's datacontext is the same ViewModel where the above DCV is located …

           <toolkit:DataForm                          

               x:Name="RegionPropertiesEditForm"

                   ItemsSource="{Binding RegionCollectionView}"

                   AutoGenerateFields="False"  

                   AutoEdit="True"

                   AutoCommit="False"

                   CommitButtonContent="Submit"

                   CommandButtonsVisibility="Cancel, Commit, Edit, Add, Navigation"        

                   Header="Region Properties"  

                   HorizontalAlignment="Stretch"

                   Visibility="{Binding DataContext.SelectedOrganizationalTreeNodeType, ElementName=LayoutRoot,  Converter={StaticResource IsOnRegionLevelConverterVisibilityConverter}, ConverterParameter=DataContext}"

                    >

                   <toolkit:DataForm.EditTemplate>

                       <DataTemplate>

                           <StackPanel >

                               <toolkit:DataField Name="DescriptionFieldName" MinWidth="20"  LabelPosition="Top" Label="Description"

                                                                                          >

                                   <TextBox Name="Description" Text="{Binding  Description, Mode=TwoWay}"  

                                        />

                               </toolkit:DataField>

                           </StackPanel>

                       </DataTemplate>

                   </toolkit:DataForm.EditTemplate>

                   <toolkit:DataForm.ReadOnlyTemplate>

                       <DataTemplate>

                           <StackPanel >

                               <toolkit:DataField Name="DescriptionFieldName" MinWidth="20"  LabelPosition="Top" Label="Description"

                                                                                          >

                                   <TextBox Name="Description" Text="{Binding  Description, Mode=OneWay}"  

                                        />

                               </toolkit:DataField>

                           </StackPanel>

                       </DataTemplate>

                   </toolkit:DataForm.ReadOnlyTemplate>

                   <i:Interaction.Triggers>

                       <i:EventTrigger EventName="EditEnded">

                           <gs:EventToCommand PassEventArgsToCommand="True"  Command="{Binding OrganizationalEditEndedViewCommand, Mode=OneWay}"/>

                       </i:EventTrigger>

                   </i:Interaction.Triggers>

               </toolkit:DataForm>

  8. weitzhandler says:

    Hi Kyle and thanks for your post!

    I wish there could have been a single class that wraps up all the components to one unified type.

    An example would be:

    public class DomainCollectionManager<TContext, TEntity>

     where TContext : DomainContext, new()

     where TEntity : Entity

    { }

    This class should have multiple overloads, some of them that provide Expression<Func<TContext, EntitySet<TEntity>>> to get the entity set, some will instantiate the TContext by its own; some should take a query (Expression<Func<TContext, EntityQuery<TEntity>>>) and a regular method should be on the class "Load" – the query has already been specified in the constructor.

    In my opinion, the tool doesn't have to expose anything but the view.

    The query should be specified at constructions or even later by an expression lambda selecting from the TContext.

    Thank you Kyle

  9. weitzhandler says:

    Hello!

    I was unable to use it.

    The type 'System.ComponentModel.IPagedCollectionView' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Windows.Data, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.

  10. weitzhandler says:

    Sorry for posting previous comment. I guess the reference somehow slipped out from my project.

  11. cleo says:

    Hello,

    When records are sorted (with SortDescriptions) and grouped (with GroupDescriptions) on different fields and then paged, the result is not coherent.

    Records seem first sorted on the server with fields in GroupDescriptions and then the rows in the page sorted (locally) with fields in SortDescriptions?

    This should not it be reversed or the fields in the GroupDescriptions put after the fields in the SortDescriptions to build the Order part of the SQL query ?

  12. kylemc says:

    @cleo

    It was an intentional choice to apply grouping before sorting in the DCV. The keeps it aligned with the DomainDataSource and (I belive) other collection view implementations. However, this logic is not (strictly) part of the DCV, but instead is defined in the SortAndPageBy extension method that is used in most Load callbacks. You can be more explicit about the sorting query that's sent to the server by using CollectionViewExtensions.SortBy(QueryBuild, SortDescriptions, GroupDescriptions) if you'd like. For instance, if you don't pass the GroupDescriptions in, the sorts will be applied on the server, but the groups will only be applied locally.

  13. HI kyle, so I was trying to bind a DCV to 2 datagrids, for master – detail scenario, my question is,

    Is there a way to do this in a many to many relationship with 3 tables, for instance a Users, table aand a Roles table both with a relationship witha third table called UsersInRoles.

    I've tried this.

    Bind the firs DataGrid to the DCV od users, and bing the second Datagrid to the same DCV but to the UsersInRole.Role but It doesnt display any members.

    Is the DCV suitable only for 2 entity graph? is there away to get the members of the Roles table that relate to the Users with the same DCV or should I declare a second DCV and bind it to the 2nd DataGrid and handle the filters in the ViewModel?

    Thank you

  14. kylemc says:

    @freddyccix

    The DCV should be suitable for any setup where you can navigate from one entity to another. However, the binding for your scenario might be tricky. I assume you're loading Users, UsersInRole, and Role all at once. Also, I'm assuming User has a collection of UsersInRole object each with a reference to a Role. Finally, I'm assuming you want the Roles for the selected User to show up in the second DataGrid.

    If so, I think you'll want to bind the second DataGrid.ItemsSource to a collection; specifically UsersInRole on the selected User. Once you have data showing up for the intermediate table, you should be able to manually set up the columsn and the bindings so you're only seeing data for the Role.

  15. Thanks Kyle, it actually worked by manually binding the second datagrid to the first navigation property and the columns of the grid to the next navagtion property.

    now I have this problem, I'm using a hierarchies table to map area entities. For now I think of only one control to display this : the TreeView control.

    The problem is that I selected an item from the treeview (wich is bind to de DVC) and it doesn't map to the DCV. I noticed this when I registered to the SelectionChanged event for the treeview and inspect de DCV's CurrentItem property and it was null.

    is there a way to make this work? cause the DCV's currentitem is readonly

  16. kylemc says:

    @freddyccix

    The DCV's CurrentItem can be updated using the "MoveCurrentToXx" methods. I'm not sure why the TreeView doesn't do this automatically, but you can certainly update it in the SelectionChanged event.

  17. Noah Wollowick says:

    Hi Kyle – Great stuff!

    Using the newest release – FYI – The control doesn't seem to work if there is no Add (Insert) functionality in the Domain Service layer. I got it working by just adding that (even though I won't use it) so didn't explore any deeper. Thought you might want to know… Thx again for this great stuff.

  18. kylemc says:

    @Noah

    What control are you talking about specifically? I remember thinking about how this worked in the design, but the details escape me at the moment. IIRC, customizing the UI is left to the developer. If you don't support 'Add' on the server, then you need to hide the 'Add' button on the client. Also, there's a degree of control based on the source collection you create the DomainCollectionView with. For instance, if you just pass in an array, it knows it won't be able to add to it.

  19. Kerry says:

    Hi Kyle,

    I'm currently binding 2x DomainCollectionViews that have a association between them into a Treeview:

     <sdk:TreeView x:Name="ParentList" BorderThickness="0,0,0,0"  ItemsSource="{Binding Parents}" >

                   <sdk:TreeView.ItemTemplate>

                       <sdk:HierarchicalDataTemplate ItemsSource="{Binding Children}">

                           <StackPanel>

                               <TextBlock Text="{Binding DisplayName}"></TextBlock>

                           </StackPanel>

                       </sdk:HierarchicalDataTemplate>

                   </sdk:TreeView.ItemTemplate>

               </sdk:TreeView>

    The Treeview Displays correctly but the DomainCollectionview.CurrentChanged on either view doesn't fire when a node is selected. If I add the SelectedItemChanged event to the treeview that fires so I'm puzzled to why the domaincollectionview isn't catching the change also.  

    Am I missing something? Any help would be most appreciated

  20. sten says:

    how do I access any documentation? How can I see all the API's and such available?

  21. kylemc says:

    @sten

    Hmm… good question. You best options are (1) IntelliSense, (2) Reflector (or some other IL reader), or (3) the VS object browser. You can refer to my post(s) for the primary types (DomainCollectionView, EntityList, and QueryBuilder). It turns out there isn't a page where you can just go read through the documentation.

  22. weitzhandler says:

    Please make available via NuGet, thanks a lot!

  23. kylemc says:

    It's one of the many RIA Services packages already available.

    nuget.org/…/RIAServices.ViewModel

  24. duluca says:

    I'm a bit unsure as to how the grouping should work. I added this line to the sample:

    this.CollectionView.GroupDescriptions.Add(new PropertyGroupDescription("Int32"));

    I changed how Int32 is being assigned as key % 2, so there should be two resulting groups. However, the group counts only show what's on the first page. So in this case, it'll show Group 0, as having an item count of 10, which is clearly incorrect.

    Is grouping and paging supposed to be working in tandem?