Two-way binding problem with WPF ComboBox using MVVM
I have an Activity
object with many properties. One of them is as follows:
public ActivityStatus Status
{
get { return status; }
set { status = value; NotifyPropertyChanged("Status"); }
}
The ActivityStatus
class has just two properties:
public Guid Guid
{
get { return guid; }
set { guid = value; NotifyPropertyChanged("Guid"); }
}
public string Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
and the Equals
methods:
public override bool Equals(object otherObject)
{
if (!(otherObject is ActivityStatus)) return false;
return Equals(otherObject as ActivityStatus);
}
public bool Equals(ActivityStatus otherStatus)
{
if (!(otherStatus is ActivityStatus) || otherStatus == null) return false;
return Guid == otherStatus.Guid && Name == otherStatus.Name;
}
I have an ActivityViewModel
class as the DataContext
of an ActivityView
class. The ActivityViewModel
has an Activity
property of type Activity
and among others, an ActivityStatuses
property of type ObservableCollection<ActivityStatus>
. In the ActivityView
I have a ComboBox
declared as follows:
<ComboBox ItemsSource="{Binding ActivityStatuses}"
SelectedItem="{Binding Activity.Status, Mode=TwoWay}"
DisplayMemberPath="Name" />
This allows me to select an ActivityStatus
from the ComboBox
and this correctly updates the Status
property of the Activity
in the Activity
property of the viewmodel. The problem is with the two-way binding... when loading a new Activity
, the ComboBox.SelectedItem
does not update to show the Activity.Status
property value.
Using this declaration of the ComboBox
, the SelectedItem
is bound to the ActivityStatus
object in the Activity
and this is a different object to the one with the same values in the viewmodel ActivityStatuses
property. Therefore the WPF Framework does not think that the items are the same and does not select the item in the ComboBox
.
If I assign the item from the collection with the same values to the Activity.Status
property after loading each Activity
, then the ComboBox
finds a match in its ItemsSource
collection and correctly sets the SelectedItem
property displaying the value. I don't really want to have to do this though because I have many other similar properties in th Activity
class and I'll have to repeat this code anywhere I want to two-way bind to ComboBox
es.
So I also tried binding to ActivityStatus.Guid
property as follows:
<ComboBox ItemsSource="{Binding ActivityStatuses}"
SelectedValue="{Binding Activity.Status.Guid, Mode=T开发者_C百科woWay}"
SelectedValuePath="Guid"
DisplayMemberPath="Name" />
This correctly selected the object with the same Guid
as the one in the Activity.Status
property from the ComboBox.ItemsSource
collection when loading different Activity
objects. The problem with this method is that the SelectedValue
is bound to the ActivityStatus.Guid
property in the ActivityStatus
object and and so when changing values in the UI, only the 'Guid' property of the ActivityStatus
object would update, leaving the name unchanged. The object in the Activity.Status
property does not change except for the value of its Guid
property.
As you can see, I also tried implementing the Equals
method as I assumed that the ComboBox
would use this to compare the objects, but it didn't make any difference. So finally, I am at a loss and keen to find a simple clean way to fix this problem... hopefully, there's a simple property that I've missed on the ComboBox
.
I simply want to be able to select an item in the ComboBox
and have the Activity.Status
object change accordingly and change the value of the Activity.Status
property from code and have the ComboBox.SelectedItem
also update accordingly. I'd be grateful for any advice.
UPDATE >>>
After reading Will's response, I tried his code sample in a new solution and saw that it worked as expected. I then examined his code thorouhly and saw that it was the same as mine, so ran my own solution again (for the first time since this post). To my complete surprise, it worked as expected without me changing any code!
This puzzled me greatly and I've spent some time to find out what happened. It turns out that the problem was/is Visual Studio 2010! I had added the Equals
methods to my data types as the last stage. For some reason Visual Studio did not build the data types project when running the application.
So the application must have been using an older dll file and my changes were not being used... I did wonder why my break points on the Equals
methods were never hit. This led to my assumption that implementing the Equals
methids did not help. Visual Studio has the same behaviour today and that's how I found out what had happened.
I checked the project build order in my solution, but that lists the data types project in the correct place in the order. When running the application though, the Output window in Visual Studio shows the project dlls being loaded in a different order. I'm not sure why running the application no longer does a complete build, but at least I know that I have to build that project after making changes in it before running the application.
FINAL UPDATE >>>
I just found out why my data types project was not building... I looked in the Configuration Manager window and saw that somehow the Platform was incorrect for that project and the Build checkbox had become unchecked! I have no idea how this happened, but am much relieved that I finally got to the bottom of the problem.
I have some bad news for you. It should work. There is a bug/unexpected side effect somewhere else that is causing your problem.
I threw together a quick project to do what you're trying to do. Like to see it here it goes.
Create a new WPF project called NestedProperties. Add a new class to the root and paste the following code (I've removed lots of stuff so it is a bit ugly):
public sealed class ViewModel : DependencyObject
{
public ObservableCollection<Activity> Activities
{ get; private set; }
public ObservableCollection<ActivityStatus> Statuses
{ get; private set; }
public static readonly DependencyProperty
SelectedActivityProperty =
DependencyProperty.Register(
"SelectedActivity",
typeof(Activity),
typeof(ViewModel),
new UIPropertyMetadata(null));
public Activity SelectedActivity
{
get { return (Activity)GetValue(SelectedActivityProperty); }
set { SetValue(SelectedActivityProperty, value); }
}
public ViewModel()
{
Activities = new ObservableCollection<Activity>();
Statuses = new ObservableCollection<ActivityStatus>();
// NOTE! Each Activity has its own ActivityStatus instance.
// They have the same Guid and name as the instances in
// Statuses!!
for (int i = 1; i <= 4; i++)
{
var id = Guid.NewGuid();
var aname = "Activity " + i;
var sname = "Status " + i;
Activities.Add(new Activity
{
Name = aname,
Status = new ActivityStatus
{
Name = sname,
Id = id,
InstanceType = "Activity"
}
});
Statuses.Add(new ActivityStatus
{
Name = sname,
Id = id,
InstanceType = "Collection"
});
}
}
}
public sealed class Activity : DependencyObject
{
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name",
typeof(string),
typeof(Activity),
new UIPropertyMetadata(null));
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty StatusProperty =
DependencyProperty.Register(
"Status",
typeof(ActivityStatus),
typeof(Activity),
new UIPropertyMetadata(null));
public ActivityStatus Status
{
get { return (ActivityStatus)GetValue(StatusProperty); }
set { SetValue(StatusProperty, value); }
}
}
public sealed class ActivityStatus
{
public Guid Id { get; set; }
public string Name { get; set; }
/// <summary>
/// indicates if this instance came from
/// the ComboBox or from the Activity
/// </summary>
public string InstanceType { get; set; }
public ActivityStatus()
{
Id = Guid.NewGuid();
}
public override bool Equals(object otherObject)
{
if (!(otherObject is ActivityStatus)) return false;
return Equals(otherObject as ActivityStatus);
}
public bool Equals(ActivityStatus otherStatus)
{
if (!(otherStatus is ActivityStatus) ||
otherStatus == null) return false;
return Id == otherStatus.Id &&
Name == otherStatus.Name;
}
}
Now open up MainWindow and paste this in:
<Window
x:Class="NestedProperties.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
xmlns:t="clr-namespace:NestedProperties"
SizeToContent="Height"
MaxHeight="350"
Width="525">
<Window.DataContext>
<t:ViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="auto" />
<RowDefinition
Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label>Select an Activity:</Label>
<ComboBox
Grid.Row="1"
ItemsSource="{Binding Activities}"
SelectedItem="{Binding SelectedActivity}"
DisplayMemberPath="Name" />
<Label
Grid.Column="1">Select a Status</Label>
<ComboBox
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding Statuses}"
SelectedItem="{Binding SelectedActivity.Status}"
DisplayMemberPath="Name" />
<ContentControl
Grid.Row="2"
Grid.ColumnSpan="2"
Content="{Binding SelectedActivity}">
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel>
<Label>Selected Activity:</Label>
<TextBlock
Text="{Binding Name}" />
<Label>Activity Status</Label>
<TextBlock
Text="{Binding Status.Name}" />
<Label>Status Id</Label>
<TextBlock
Text="{Binding Status.Id}" />
<Label>Status came from</Label>
<TextBlock
Text="{Binding Status.InstanceType}" />
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Window>
When you run this, you'll find you have four Activities and four Statuses. If you flip through the Activities combo, you'll see each Status is marked as Activity, meaning it is the instance given to the Activity in the constructor of the ViewModel. You will also see that the Status combobox changes as the Activity changes, meaning that the Equals
method is working.
Next, change the status for each Activity. You'll see the type of the status changes to Collection, meaning that this instance was created and added to the Statuses collection in the constructor.
So why is this working, but your code isnt? I'm not sure. Your problem lies elsewhere in your code.
Man I don't know if I followed your question exactly, but when you say
when loading a new Activity,
Are you adding that new Activity
to your ActivityStatuses
collection? Because if you're not then I'm pretty sure the binding won't work because the SelectedItem needs to be in the ItemsSource.
Just a thought.
精彩评论