4th Dimension
4th Dimension

Reputation: 65

Databinding Entity Framework navigation properties - handling change

So I'm building my first larger application and I'm using WPF for Windows and stuff and Entity Framework for retrieving, updating and storing data.So far using a pattern similar to the MVVM pattern, I had a couple of issues but was able to resolve them and am quite far into design. Also, I'm using database first approach.

But I have just ran into a brick wall that I should have anticipated. It has to do with nested properties in entities and the way changes to them are handled. Let's explain.

For the purpose of simplicity I will not be using my actual class names. So let's say I have three entities in my EF Model: Department, Manager and PersonalInfo. I modified my *.tt Template file so that all my entities also implement INotifyPropertyChanged interface, but only for their NON NAVIGATION properties since Navigation properties are declared as virtual and WILL be overridden by EF when their date gets set.

So let's say my generated classes look like this:

public partial class Department : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropChange(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    public Department() { }

    int _id;
    public int ID { get { return _id; } set { _id = value; OnPropChange("ID"); } }

    int _someproperty;
    public int SomeProperty { get { return _someproperty; } set { _someproperty= value; OnPropChange("SomeProperty"); } }

    int _managerid;
    public int ManagerID { get { return _managerid; } set { _managerid = value; OnPropChange("ManagerID"); } }
    public virtual Manager Manager { get; set; }
}

public partial class Manager : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropChange(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
    public Manager() { }

    int _id;
    public int ID { get { return _id; } set { _id = value; OnPropChange("ID"); } }

    public virtual PersonalInfo PersonalInfo { get; set; }
}

public partial class PersonalInfo : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropChange(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    public PersonalInfo() { }

    int _id;
    public int ID { get { return _id; } set { _id = value; OnPropChange("ID"); } }

    string _firstname;
    public string FirstName { get { return _firstname; } set { _firstname = value; OnPropChange("FirstName"); } }

    string _lastname;
    public string LastName { get { return _lastname; } set { _lastname = value; OnPropChange("LastName"); } }
}

Now this works pretty well if I want to let's say display a list of Departments with their Managers. First I load the data into the EF Context like so

Context.Departments.Include(d => d.Manager.PersonalInfo).Load();
Departments = Context.Deparments.Local;

And than in the XAML I can do:

<DataGrid ItemsSource="{Binding Departments}" SelectedItem="{Binding CurrentDepartment, Mode=TwoWay}">
   <DataGrid.Columns>
       <DataGridTextColumn Binding="{Binding ID}"/>SomeProperty 
       <DataGridTextColumn Binding="{Binding SomeProperty }" Header="Property"/>
       <DataGridTextColumn Binding="{Binding Manager.PersonalInfo.FirstName}" Header="FirstName"/>
       <DataGridTextColumn Binding="{Binding Manager.PersonalInfo.LastName}" Header="LastNameName"/>
   </DataGrid.Columns>
</DataGrid>

And all of this works wonderfully. I can add and remove items with no problems by simply removing them from Context and saving changes. Since entity sets are ObservableCollections any additions or removal from them automatically raises appropriate events which update the datagrid. I can also modify any nonnavigation property of the Department and can refresh the data in CurrentDepartment like so:

 Context.Entry(CurrentDepartment).Refresh();

and it automatically refreshes the data in the datagrid.

Problems start when I change one of the navigation properties. Let's say that I opened a window in which I edited the Department, where I changed the current manager from Bob Bobington to Dave Daveston. When I return to this window calling:

 Context.Entry(CurrentDepartment).Refresh();

It will only refresh non navigation properties, and First and Lastname columns will still say Bob Bobington. But that is Refresh function working as intended. But if I load the correct data into the context like this:

  Context.Entry(CurrentDepartment).Reference(d=>d.Manager);
  Context.Entry(CurrentDepartment.Manager).Reference(m=>m.PersonalInfo);

is still won't change the contents of the first and last name columns since they are still bound to the OLD manager. They will only refresh if the change happens on Bob Bobington instance of PersonalInfo.

I can sort of solve this level of problem by binding the column directly to Manager property, and converting Manager to text either via a ValueConverter or by overriding ToString for Manager. But that won't help since WPF won't ever be notified that Manager property has changed since changes to that property don't raise PropertyChanged event.

Navigation properties can not raise that event since even if I edited the tt template so it generates the code for the navigation property like so:

 Manager _manager;
 public virtual Manager Manager { get{return _manager;}  
      set{
           _manager=value;
           OnPropChange("Manager");
      }
 }

all this code will likely be overridden by the EF framework itself.

Sooo, what is the best thing to do in these cases? Please don't tell me that conventional wisdom is to copy the data from EF Poco classes into your own and use them. :(

UPDATE:

Here goes a potentially stupid solution for this problem. But it works.

ObservableCollection<Department> tempd = Departments;
Department temp = CurrentDepartment;
Departments = null;
CurrentDepartment = null;

Context.Entry(temp).Refresh();
Context.Entry(temp).Reference(d=>d.Manager).Load();
Context.Entry(temp.Manager).Reference(m=>m.PersonalInfo).Load();

Departments = tempd;
CurrentDepartment = temp;

As you can clearly see the key is in forcing the DataGrid to rebind itself from scratch. This way it will use no shortcuts and will rebind itself properly. BUT this method is quite silly. I shiver at the thought of having to do this to datagrids with hundreds of rows.

So I'm still waiting for a proper solution, but I'll be continuing onwards using this. Something is better than nothing.

Upvotes: 3

Views: 2789

Answers (1)

pete the pagan-gerbil
pete the pagan-gerbil

Reputation: 3166

Well, conventional wisdom is to copy the data across to another POCO, or at least make your ViewModel class peek through to an underlying Model class. You have combined your Model and ViewModel classes such that Model-based constraints (virtual methods required by your ORM) are interfering with your ViewModel-based constraints (to allow databinding, you must be able to raise events from property setters).

If your Model and ViewModel were properly separated (Separation of Concerns) then you could have your virtual methods and database-required fields on your Model (a DB persistable object) and your purely View-based functions (PropertyChanged events) on your ViewModel. Your DB code should never care about your PropertyChanged events anyway.

You can make it easier by making the ViewModel a look-through class so every property getter-setter looks like:

public string PropertyThing
{
    get { return _myModel.PropertyThing; }
    set { _myModel.PropertyThing = value; PropChanged("PropertyThing"); }
}

If you're already doing code generation this shouldn't be a major chore.

Alternatively, you could duplicate all the values with something like AutoMapper to separate out your Model and ViewModel to separate classes.

It's not what you wanted to hear, but your ORM and your UI are clashing and that's the sort of thing that MVVM architecture (specifically separating the Model and ViewModel) are supposed to make better.

Upvotes: 1

Related Questions