Проблема двустороннего связывания с WPF ComboBox с использованием MVVM - PullRequest
12 голосов
/ 27 мая 2011

У меня есть 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 класс как DataContext класса ActivityView. ActivityViewModel имеет свойство Activity типа Activity и, среди прочего, свойство ActivityStatuses типа ObservableCollection<ActivityStatus>. В ActivityView у меня есть ComboBox, объявленный следующим образом:

<ComboBox ItemsSource="{Binding ActivityStatuses}" 
          SelectedItem="{Binding Activity.Status, Mode=TwoWay}"
          DisplayMemberPath="Name" />

Это позволяет мне выбрать ActivityStatus из ComboBox и корректно обновляет свойство Status Activity в свойстве Activity модели представления. Проблема заключается в двусторонней привязке ... при загрузке нового Activity ComboBox.SelectedItem не обновляется, чтобы показать значение свойства Activity.Status.

Используя это объявление ComboBox, SelectedItem привязывается к объекту ActivityStatus в Activity, и этот объект отличается от объекта с теми же значениями в свойстве viewmodel ActivityStatuses. Поэтому WPF Framework не считает, что элементы совпадают, и не выбирает элемент в ComboBox.

Если я присваиваю элемент из коллекции с такими же значениями свойству Activity.Status после загрузки каждого Activity, тогда ComboBox находит совпадение в своей коллекции ItemsSource и правильно устанавливает свойство SelectedItem отображение значения. Я действительно не хочу этого делать, потому что у меня есть много других аналогичных свойств в классе Activity, и мне придется повторять этот код везде, где я хочу, чтобы выполнить двустороннее связывание с ComboBox es.

Поэтому я также попытался связать свойство ActivityStatus.Guid следующим образом:

<ComboBox ItemsSource="{Binding ActivityStatuses}" 
          SelectedValue="{Binding Activity.Status.Guid, Mode=TwoWay}"
          SelectedValuePath="Guid" 
          DisplayMemberPath="Name" />

Это правильно выбрал объект с тем же Guid, что и у свойства Activity.Status из коллекции ComboBox.ItemsSource при загрузке различных объектов Activity. Проблема этого метода заключается в том, что SelectedValue привязан к свойству ActivityStatus.Guid в объекте ActivityStatus, и поэтому при изменении значений в пользовательском интерфейсе обновляется только свойство «Guid» объекта ActivityStatus, оставив имя без изменений. Объект в свойстве Activity.Status не изменяется, за исключением значения его свойства Guid.

Как вы можете видеть, я также пытался реализовать метод Equals, так как предполагал, что ComboBox будет использовать это для сравнения объектов, но это не имело никакого значения. Итак, наконец, я в растерянности и стремлюсь найти простой чистый способ решения этой проблемы ... надеюсь, есть простое свойство, которое я пропустил в ComboBox.

Я просто хочу иметь возможность выбрать элемент в ComboBox и соответственно изменить объект Activity.Status и изменить значение свойства Activity.Status из кода, а также обновить ComboBox.SelectedItem соответственно. Буду благодарен за любой совет.

ОБНОВЛЕНИЕ >>>

Прочитав ответ Уилла, я попробовал его пример кода в новом решении и увидел, что он работает как ожидалось. Затем я внимательно изучил его код и увидел, что он такой же, как и у меня, поэтому снова применил свое собственное решение (впервые с этого поста). К моему полному удивлению, это сработало, как и ожидалось, без изменения кода!

Это сильно озадачило меня, и я потратил некоторое время, чтобы выяснить, что случилось. Оказывается, проблема была / есть Visual Studio 2010! В качестве последнего этапа я добавил методы Equals к своим типам данных. По какой-то причине Visual Studio не создавал проект типов данных при запуске приложения.

Таким образом, приложение, должно быть, использовало более старый файл dll, и мои изменения не использовались ... Я действительно удивлялся, почему мои точки останова на методах Equals никогда не были достигнуты. Это привело к моему предположению, что реализация метидов Equals не помогла. Visual Studio сегодня ведет себя так же, и вот как я узнал, что произошло.

Я проверил порядок сборки проекта в своем решении, но в нем перечислены типы данных проекта в правильном месте в порядке.Однако при запуске приложения в окне «Вывод» в Visual Studio отображается библиотека проекта, загружаемая в другом порядке.Я не уверен, почему при запуске приложения больше не выполняется полная сборка, но, по крайней мере, я знаю, что мне нужно собрать этот проект после внесения в него изменений перед запуском приложения.1093 *

Я только что узнал, почему мой проект типов данных не строился ... Я посмотрел в окне Configuration Manager и увидел, что Платформа каким-то образом неверна для этого проекта, и флажок Build стал непроверенным!Я понятия не имею, как это произошло, но я очень рад, что наконец понял проблему.

Ответы [ 2 ]

12 голосов
/ 27 мая 2011

У меня для вас плохие новости. Он должен работать. В другом месте есть ошибка / неожиданный побочный эффект, который вызывает вашу проблему.

Я собрал быстрый проект, чтобы сделать то, что ты пытаешься сделать. Хотелось бы увидеть это здесь.

Создайте новый проект WPF под названием NestedProperties. Добавьте новый класс в корень и вставьте следующий код (я удалил много вещей, так что это немного некрасиво):

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>

Когда вы выполните это, вы обнаружите, что у вас есть четыре действия и четыре статуса. Если вы пролистаете комбо «Деятельности», вы увидите, что каждый Статус помечен как Активность , что означает, что это экземпляр, данный Деятельности в конструкторе ViewModel. Вы также увидите, что поле со списком Status изменяется при изменении действия , что означает, что метод Equals работает.

Затем измените статус для каждого действия. Вы увидите тип изменения статуса на Коллекция , означающий, что этот экземпляр был создан и добавлен в коллекцию Статусов в конструкторе.

Так почему это работает, а ваш код - нет? Я не уверен. Ваша проблема лежит в другом месте вашего кода.

0 голосов
/ 27 мая 2011

Человек, я не знаю, точно ли я следил за вашим вопросом, но когда вы говорите

при загрузке нового занятия,

Вы добавляете это новое1007 * в вашу ActivityStatuses коллекцию?Потому что если нет, то я почти уверен, что привязка не сработает, потому что SelectedItem должен быть в ItemsSource.

Просто мысль.

...