Improved binding support in EntitySet and EntityCollection
For our WCF RIA Services V1.0 SP1 Beta release we were finally able to improve binding support for EntitySets and EntityCollections. In our V1 release, EntitySet and EntityCollection did not support adding and removing entities through the IEditableCollectionView interface when used as the ItemsSource in a DataGrid or DataForm. While the details of the problem will only be familiar to some, if you’ve had difficulty adding or removing in a master/details scenario, this is the likely cause.
A Master/Details Example
In this section, I’ll put together a short sample showing how this feature enables a simple master/details scenario. I want to focus mainly on the client side of things so we’ll keep the DomainService simple.
[EnableClientAccess]
public class SampleDomainService : DomainService
{
public IEnumerable<SampleEntity> GetEntities() {…}
public void CreateEntity(SampleEntity entity) {…}
public void UpdateEntity(SampleEntity entity) {…}
public void DeleteEntity(SampleEntity entity) {…}
}
public class SampleEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
When the RIA Services codegen runs, it will generate the SampleDomainContext and SampleEntity types on the client. I’ve omitted most of the generated code, but here are the two members we’ll use later in the sample.
public class SampleDomainContext : DomainContext
{
public EntitySet<SampleEntity> SampleEntities
{
get {…}
}
public EntityQuery<SampleEntity> GetEntitiesQuery() {…}
}
My approach on the client side of the application is simple. I use a DataGrid to show a list of the SampleEntities and the DataForm to add, remove, and edit. I’ve defined my SampleDomainContext in the xaml to make the binding simple and explicit.
<UserControl …>
<UserControl.Resources>
<web:SampleDomainContext x:Key="myDomainContext"/>
</UserControl.Resources>
<StackPanel x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Horizontal" Margin="2">
<sdk:DataGrid Name="myDataGrid"
ItemsSource="{Binding SampleEntities,
Source={StaticResource myDomainContext}}"
IsReadOnly="True" />
<toolkit:DataForm Name="myDataForm" Width="200"
ItemsSource="{Binding SampleEntities,
Source={StaticResource myDomainContext}}"
CurrentItem="{Binding SelectedItem,
ElementName=myDataGrid, Mode=TwoWay}"
CommandButtonsVisibility="Add,Delete,Commit,Cancel" />
</StackPanel>
<Button Name="Submit" Content="Submit Changes" Click="Submit_Click"
HorizontalAlignment="Left" Margin="2"/>
</StackPanel>
</UserControl>
The code behind is just as simple. I pull the SampleDomainContext from the resources (that I declared in xaml) and use it to load and save the SampleEntities. The only complexity here is tracking the DataForm’s Editing state to make sure the edit is committed and the validation errors are addressed before submitting changes.
public partial class MainPage : UserControl
{
private readonly SampleDomainContext _context;
private bool _isDataFormEditing;
public MainPage()
{
InitializeComponent();
this._context =
(SampleDomainContext)this.Resources["myDomainContext"];
this._context.Load(this._context.GetEntitiesQuery());
// This is an unfortunate workaround to track the
// DataForm Editing state
this.myDataForm.AddingNewItem +=
(sender, e) => this._isDataFormEditing = true;
this.myDataForm.BeginningEdit +=
(sender, e) => this._isDataFormEditing = true;
this.myDataForm.EditEnded +=
(sender, e) => this._isDataFormEditing = false;
}
private void Submit_Click(object sender, RoutedEventArgs e)
{
if (!this._isDataFormEditing || this.myDataForm.CommitEdit())
{
this._context.SubmitChanges();
}
}
}
In the end, I have a nice little Master/Details application.
To Sum it Up
This sample is pretty simple. The point I really want to emphasize here is that you can now bind EntitySets and EntityCollections to Silverlight controls like the DataGrid and DataForm and have them work like you would expect them to. On top of that, these types will show improved compatibility in features like drag-and-drop and as well as in third party controls.
Comments
Anonymous
October 29, 2010
So which/where/what is the EntitySet or EntityCollection? Those types appear in the title of your blog but I do NOT see them anywhere in the code samples you present. Not very helpful for befuddled beginners who are not WCF RIA Services Gurus. How many more iterations before we get clear coherent comprehensive practical and useable docs and demos for those of us who are no where near as smart as the Gods presenting at PDC10?Anonymous
October 29, 2010
Good point. I'll update the post to show the generated code. (but until I do, SampleDomainContext.SampleEntities is an EntitySet<SampleEntity>)Anonymous
October 31, 2010
If I expose the EntitySet directly in my viewmodel, how do I keep it in sync with the DomainContext? // MyViewModel public EntitySet<User> Users { get { return this.Context.Users; } } private void InsertUser() { User user = new User() { Username = "user" }; this.Context.Users.Add(user); this.Context.SubmitChanges( operation => { if (operation.HasError) { operation.MarkErrorAsHandled(); // ---> Is this really necessary? this.Context.Users.Remove(user); } // Issue another request to reload the entityset LoadData(); } , null); } Also, should I manually issue another request to reload the entityset everytime I submit changes (just in case somebody else did modify the collection) or is there a way to do automatically? I've seen people overriding the Load method in a partial class for the client-side DomainContext: public override LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action<LoadOperation> callback, object userState) { return base.Load(query, LoadBehavior.RefreshCurrent, callback, userState); } Thanks.Anonymous
November 01, 2010
Your EntitySet is always 'in sync' with your DomainContext, so I'm not positive quite what your question is. I'll try to answer based on your code, though. First, it shouldn't be necessary to remove a user on an error (there's the possibility it's a validation error or something else you can address and resubmit). In some scenarios it might be a good idea, but I'm not sure there's a general rule. In this one, calling Context.RejectChanges() might also be an option to consider. Second, if you want to pick up new entity additions or edits to entities you did not modify, you'll need to reload the data. In this case, it won't matter what LoadBehavior you use because all the entities will be in an 'unmodified' state. It sounds to me like you do want to reload the data for your scenario, but again there isn't a general rule for this.Anonymous
November 01, 2010
Sorry, the question was that given the above scenario, binding directly to the EntitySet leads to a very awful user experience (IMHO). I mean, as soon as the newly created entity is added to the context: this.Context.Users.Add(user); it'll be shown in the DataGrid. Thereafter, when the Context.SubmitChanges() call fails, the entity is removed from the Context, either by reloading or calling Context.RejectChanges(). I think what the user would expect is the entity not to be shown in the DataGrid unless the Context.SumbmitChanges() success. Now, i can achieve exactly this by wrapping the EntitySet in a PagedCollectionView, but not binding directly to the EntitySet.Anonymous
November 01, 2010
I see. Yes, binding to an EntitySet gives you the binding experience you're describing. It's good for some scenarios, but maybe not the best when you're adding an entity (say from a details windows). In this case, it's fine to continue to use a view if that's the best behavior. I know the default binding support won't be exactly what people want for every scenario. I'm still working on a CollectionView implementation that will provide all this flexibility with RIA, but more on that later... ;)Anonymous
December 07, 2010
Kylemc, Do you have the code above in VB? I have a datagrid and a dataform and I want to be able to add information to the table from the dataform. Right now Im able edit the information on the table, but the add button is grey out.Anonymous
December 08, 2010
Hi, if you don't want make the grid sync with datacontext, add ToList() into Users property. public EntitySet<User> Users { get { return this.Context.Users.ToList(); } }Anonymous
December 08, 2010
Hi, if you don't want make the grid sync with datacontext, add ToList() into Users property. public EntitySet<User> Users { get { return this.Context.Users.ToList(); } } i thinkdAnonymous
February 01, 2011
I followed your example here. Everything works fine except the cancel button when I am adding a new item via data form. Can you upload your project so I can compare and see if I missed anything. Thanks.Anonymous
February 01, 2011
I should have been a little precise in my previous comment. When I add a new item using Data Form and I click OK, I get validation error message. I decide to stop and I click on Cancel only to get "Object reference not set to an instance of an object." error message. What am I doing wrong and how do I fix it? Thanks.Anonymous
March 03, 2011
Aditya, Had the same problem vb.net when using a property on the view model to expose a ria enity as the current selected item. Changed the currentitem binding to be the currentitem on the Icollection view and it works. Works CurrentItem="{Binding Collection.CurrentItem,Mode=OneWay}" ItemsSource="{Binding Collection,Mode=TwoWay}" Does Not Work CurrentItem="{Binding CurrentItem,Mode=OneWay}" ItemsSource="{Binding Collection,Mode=TwoWay}" Public Property CurrentItem As Data.Server.C_CATALG Get Return _CurrentItem End Get Set(ByVal value As Data.Server.C_CATALG) If Not Object.Equals(_CurrentItem, value) Then _CurrentItem = value NotifyPropertyChanged("CurrentItem") End If End Set End Property Or am I doing something else wrong too !Anonymous
March 18, 2011
The comment has been removed