Partager via


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.

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

Comments

  • Anonymous
    April 15, 2011
    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();        }

  • Anonymous
    April 17, 2011
    Great Job!

  • Anonymous
    April 17, 2011
    The comment has been removed

  • Anonymous
    April 18, 2011
    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!

  • Anonymous
    April 18, 2011
    The comment has been removed

  • Anonymous
    May 04, 2011
    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?

  • Anonymous
    May 04, 2011
    @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); }

  • Anonymous
    May 13, 2011
    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>

  • Anonymous
    May 14, 2011
    The comment has been removed

  • Anonymous
    May 14, 2011
    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'.

  • Anonymous
    May 15, 2011
    Sorry for posting previous comment. I guess the reference somehow slipped out from my project.

  • Anonymous
    May 15, 2011
    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 ?

  • Anonymous
    May 15, 2011
    The comment has been removed

  • Anonymous
    May 19, 2011
    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

  • Anonymous
    May 20, 2011
    @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.

  • Anonymous
    June 15, 2011
    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

  • Anonymous
    June 16, 2011
    @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.

  • Anonymous
    August 19, 2011
    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.

  • Anonymous
    August 21, 2011
    @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.

  • Anonymous
    September 05, 2011
    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

  • Anonymous
    October 05, 2011
    how do I access any documentation? How can I see all the API's and such available?

  • Anonymous
    October 05, 2011
    The comment has been removed

  • Anonymous
    January 05, 2012
    Please make available via NuGet, thanks a lot!

  • Anonymous
    January 16, 2012
    It's one of the many RIA Services packages already available. nuget.org/.../RIAServices.ViewModel

  • Anonymous
    May 18, 2012
    The comment has been removed