开发者

Two-way binding problem with WPF ComboBox using MVVM

开发者 https://www.devze.com 2023-03-08 12:31 出处:网络
I have an Activity object with many properties. One of them is as follows: public ActivityStatus Status

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 ComboBoxes.

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.

0

精彩评论

暂无评论...
验证码 换一张
取 消