使用MVVM模式的WPF ComboBox双向绑定问题

13

我有一个Activity对象,其中包含许多属性。其中之一如下:

public ActivityStatus Status
{
    get { return status; }
    set { status = value; NotifyPropertyChanged("Status"); }
}

ActivityStatus类只有两个属性:

public Guid Guid
{
    get { return guid; }
    set { guid = value; NotifyPropertyChanged("Guid"); }
}
public string Name
{
    get { return name; }
    set { name = value; NotifyPropertyChanged("Name"); }
}

Equals 方法:

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;
}

我有一个ActivityViewModel类作为ActivityView类的DataContextActivityViewModel有一个类型为ActivityActivity属性和一个类型为ObservableCollection<ActivityStatus>ActivityStatuses属性,还有其他属性。在ActivityView中,我声明了一个ComboBox如下:

<ComboBox ItemsSource="{Binding ActivityStatuses}" 
          SelectedItem="{Binding Activity.Status, Mode=TwoWay}"
          DisplayMemberPath="Name" />
这使我能够从ComboBox中选择ActivityStatus,这会正确更新viewmodel中Activity属性的Status属性。问题在于双向绑定...当加载新的Activity时,ComboBox.SelectedItem不会更新以显示Activity.Status属性值。
使用此ComboBox声明,SelectedItem绑定到Activity中的ActivityStatus对象,而这个对象与ViewModel中ActivityStatuses属性中具有相同值的对象不同。因此,WPF框架认为这些项不同,并且不会选择ComboBox中的项。
如果我在每次加载Activity后将具有相同值的集合项分配给Activity.Status属性,则ComboBox会在其ItemsSource集合中找到匹配项并正确设置SelectedItem属性来显示该值。然而,我不想这样做,因为Activity类中还有许多类似的属性,我必须在任何想要双向绑定到ComboBox的地方重复此代码。
因此,我还尝试了以下方式绑定到ActivityStatus.Guid属性:
<ComboBox ItemsSource="{Binding ActivityStatuses}" 
          SelectedValue="{Binding Activity.Status.Guid, Mode=TwoWay}"
          SelectedValuePath="Guid" 
          DisplayMemberPath="Name" />
当加载不同的Activity对象时,此代码正确地从ComboBox.ItemsSource集合中选择具有与Activity.Status属性中Guid相同的对象。使用此方法的问题在于SelectedValue绑定到ActivityStatus对象中的ActivityStatus.Guid属性,因此在UI中更改值时,只有ActivityStatus对象的'Guid'属性会更新,而名称保持不变。 Activity.Status属性中的对象除其Guid属性的值外不会改变。
我也尝试了实现Equals方法,因为我认为ComboBox会使用它来比较对象,但是没有任何区别。因此,最后我很困惑,并渴望找到一个简单干净的方法来解决这个问题...希望ComboBox上有一个简单的属性,我可能错过了。
我只是想能够在ComboBox中选择项目并使Activity.Status对象相应地更改,并从代码中更改Activity.Status属性的值,并相应地更新ComboBox.SelectedItem。我会感激任何建议。
更新>>> 阅读了Will的回复后,我在新解决方案中尝试了他的代码示例,并看到它按预期工作。然后,我彻底检查了他的代码,并发现它与我的代码相同,因此再次运行了我的解决方案(自发布以来第一次)。令我完全惊讶的是,它按预期工作,而我没有改变任何代码!
这让我非常困惑,我花了一些时间来找出发生了什么。事实证明,问题是/是Visual Studio 2010!我已将Equals方法添加到我的数据类型中作为最后一个阶段。由于某种原因,Visual Studio在运行应用程序时没有构建数据类型项目。
因此,应用程序必须使用旧的dll文件,并且我的更改未被使用...我曾经想知道为什么我的断点在Equals方法上从未被触发过。这导致我认为实现Equals方法没有帮助。Visual Studio今天具有相同的行为,这就是我找出发生了什么的方式。
我检查了解决方案中的项目构建顺序,但那里正确地列出了数据类型项目的位置。但是,在运行应用程序时,Visual Studio的输出窗口显示项目dll以不同的顺序加载。我不确定为什么运行应用程序不再进行完整的构建,但至少我知道在运行应用程序之前必须先构建该项目中的更改。
最后的更新>>> 我刚刚发现我的数据类型项目为什么没有构建...我查看了Configuration Manager窗口,并发现该项目的平台不正确,并且Build复选框变为未选中状态!我不知道这是如何发生的,但很高兴终于找到了问题的根源。

你所遇到的是经典的“项目引用/文件引用”问题。你应该从解决方案中删除所有对其他项目的引用,并重新添加它们作为项目引用。文件引用在构建或更改构建类型(例如,调试/发布)时不会自动更新。永远不要浏览/bin目录以添加引用!有关项目引用的更多信息,请查看此msdn文章 - user1228
1
谢谢@威尔,但我刚发现我的数据类型项目为什么不能构建...我看了配置管理器窗口,发现那个项目的平台不正确,并且构建复选框未被选中!我不知道这是怎么发生的,但非常欣慰我终于找到了问题的根源。 - Sheridan
2个回答

12
我有一个坏消息要告诉你。它应该可以工作。但是,其他地方存在一个错误/意外副作用导致了你的问题。
我临时创建了一个项目来完成你想做的事情。看看下面的内容。
创建一个名为NestedProperties的新WPF项目。在根目录中添加一个新类,并粘贴以下代码(我已经删除了很多东西,所以它有点丑):
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;
    }
}

现在打开MainWindow并粘贴以下内容:
<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>

当您运行此代码时,您会发现有四个Activities和四个Statuses。如果您翻阅Activities下拉菜单,您会看到每个Status都被标记为“Activity”,这意味着它是在ViewModel的构造函数中给Activity传递的实例。您还会看到随着Activity的更改,Status下拉菜单也会更改,这意味着Equals方法正在起作用。
接下来,更改每个Activity的状态。您会看到状态类型更改为“Collection”,这意味着该实例是在构造函数中创建并添加到Statuses集合中的。
那么为什么这段代码可以工作,而您的代码却不能?我不确定。您的问题可能出现在代码的其他地方。

-2

我不确定我是否完全理解了你的问题,但是当你说

加载新活动时,

你是否将该新Activity添加到你的ActivityStatuses集合中?因为如果没有,我相信绑定将无法工作,因为SelectedItem需要在ItemsSource中。

这只是一个想法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接