WPF IValueConverter.ConvertBack не вызывается

У меня есть код ниже, который является упрощенным примером того, что я пытаюсь сделать. Я использую конвертер, чтобы попытаться заполнить DataGrid данными из модели, которая у меня есть. DataGrid заполняется правильно, но любые изменения в сетке не сохраняются обратно в объекты. Я указал режим как TwoWay. Когда я ставлю точку останова на метод конвертера ConvertBack, он никогда не вызывается.

Я новичок в WPF и MVVM, поэтому не понимаю, что делаю не так. Я мало что могу сделать, чтобы изменить модель, поэтому я хотел бы посмотреть, сработает ли это, если не будет явно превосходящего метода.

XAML:

<Window x:Class="SampleBindingProblem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SampleBindingProblem"
        Title="MainWindow" Height="400" Width="500">
    <Window.Resources>
        <ResourceDictionary>
            <local:ScenarioDataTableConverter x:Key="ScenarioDataTableConverter" />
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <ListBox ItemsSource="{Binding Scenarios}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <DataGrid Margin="5" ItemsSource="{Binding Path=Options, Mode=TwoWay, Converter={StaticResource ScenarioDataTableConverter}}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

Приложение.xaml.cs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace SampleBindingProblem
{
    public class ColumnInfo
    {
        public static readonly String[] ColumnLabels = new String[] { "Variable1", "Variable2", "Variable3", "Variable4", "Variable5" };
    }

    public class ScenarioOption
    {
        public String Label { get; set; }
        public String[] Variables { get; set; }
    }

    public class Scenario
    {
        public ScenarioOption[] Options { get; set; }
    }

    internal class ScenarioDataTableConverter : IValueConverter
    {
        public Object Convert (Object value, Type targetType, Object parameter, CultureInfo culture)
        {
            if (value == null)
                return (null);

            ScenarioOption[] options = (ScenarioOption[]) value;

            DataTable table = new DataTable();

            table.Columns.Add("Label", typeof(String));
            for (Int32 c = 0; c < ColumnInfo.ColumnLabels.Length; ++c)
                table.Columns.Add(ColumnInfo.ColumnLabels[c], typeof(String));
            foreach (ScenarioOption option in options)
            {
                DataRow row = table.NewRow();
                List<String> lst = new List<String>();
                lst.Add(option.Label);
                lst.AddRange(option.Variables);
                row.ItemArray = lst.ToArray();
                table.Rows.Add(row);
            }

            return (table.DefaultView);
        }

        public Object ConvertBack (Object value, Type targetType, Object parameter, CultureInfo culture)
        {
            return (null);
        }
    }

    internal class ViewModel : INotifyPropertyChanged
    {
        public void RaisePropertyChanged (String property)
        {
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        public event PropertyChangedEventHandler PropertyChanged = null;

        public ObservableCollection<Scenario> Scenarios { get; set; }

        public ViewModel ()
        {
            Scenario s1 = new Scenario();
            s1.Options = new ScenarioOption[] {
                new ScenarioOption() { Label = "Opt1", Variables=new String[] { "1", "2", "3", "4", "5" } },
                new ScenarioOption() { Label = "Opt2", Variables=new String[] { "2", "3", "4", "5", "6" } },
                new ScenarioOption() { Label = "Opt3", Variables=new String[] { "3", "4", "5", "6", "7" } },
            };
            Scenario s2 = new Scenario();
            s2.Options = new ScenarioOption[] {
                new ScenarioOption() { Label = "Opt1", Variables=new String[] { "1", "2", "3", "4", "5" } },
                new ScenarioOption() { Label = "Opt2", Variables=new String[] { "2", "3", "4", "5", "6" } },
                new ScenarioOption() { Label = "Opt3", Variables=new String[] { "3", "4", "5", "6", "7" } },
            };

            this.Scenarios = new ObservableCollection<Scenario>();
            this.Scenarios.Add(s1);
            this.Scenarios.Add(s2);
        }
    }

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup (Object sender, StartupEventArgs e)
        {
            MainWindow window = new MainWindow();
            window.DataContext = new ViewModel();
            window.ShowDialog();
        }
    }
}

person Walter Williams    schedule 06.01.2014    source источник
comment
Метод ConvertBack() в настоящее время возвращает null, потому что я не реализовал его, так как он до сих пор не вызывался.   -  person Walter Williams    schedule 07.01.2014


Ответы (2)


Конвертеры не работают таким образом, когда дело доходит до коллекций. ConvertBack будет вызываться только при замене всей коллекции. Он не будет вызываться при изменении элемента в коллекции. В вашем случае коллекция (DataView) не заменяется новым экземпляром DataView, а скорее модифицируется, и поэтому ConvertBack не вызывается.

Если вы спросите меня, я не понимаю, почему вам все равно нужно использовать конвертер. Либо привяжите непосредственно к свойству Scenarios и работайте с этой коллекцией, которая предоставляется моделью представления, либо, альтернативно, вызовите код преобразования в своей модели представления и предоставьте результирующий DataView в другом свойстве. Тогда вам просто нужно будет привязаться к этому свойству без указания преобразователя.

person Adi Lester    schedule 06.01.2014
comment
ListBox уже привязан к Scenarios, так как представление позволяет работать с несколькими сценариями. Это набор опций в сценарии, который я пытаюсь получить в сетке. Итак, вы говорите о реализации свойства в сценарии, которое получает/устанавливает DataView? - person Walter Williams; 07.01.2014
comment
Это, наверное, будет самый простой путь. Просто убедитесь, что вы удалили конвертер. - person Adi Lester; 07.01.2014
comment
Я играл с этим, и до сих пор поведение кажется точно таким же. Сеттер в DataView никогда не вызывается. - person Walter Williams; 07.01.2014
comment
И не положено. Просто посмотрите на значения, которые он содержит, и посмотрите, являются ли они новыми. - person Adi Lester; 07.01.2014
comment
Как модель или модель представления должны это делать? - person Walter Williams; 07.01.2014
comment
Опять же, устанавливаются значения внутри DataView, а не сам DataView. - person Adi Lester; 07.01.2014
comment
Это МВВМ. У меня нет переменной, указывающей на представление, просто лежащее в виртуальной машине или модели. Как и когда я должен получить значения? - person Walter Williams; 07.01.2014
comment
@WalterWilliams Из-за привязки значения всегда обновляются в модели представления. Если вы хотите отобразить их в представлении, используйте привязку. Если они вам нужны в вашей модели представления, вам доступны свойства виртуальной машины. - person Adi Lester; 07.01.2014
comment
@WalterWilliams Я считаю, что большинство ваших вопросов связано с непониманием основ MVVM. Эта статья — отличный ресурс для обновления ваших знаний. - person Adi Lester; 07.01.2014
comment
Я учусь, но я думаю, что вы не понимаете, что DataTable/DataView здесь не является моделью; DataTable/DataView — это только контейнер, чтобы правильно отображать его в представлении. Геттер для DataView в виртуальной машине каждый раз заново создает DataView из модели. Я не могу пойти и полностью изменить реализацию модели только для этого случая. - person Walter Williams; 07.01.2014

Это звучит как классическая ошибка новичка... Я думаю, вам нужно реализовать INotifyPropertyChanged Interface в классах вашей модели. Идея состоит в том, что вы информируете интерфейс INotifyPropertyChanged об изменении значения любого свойства. Со связанной страницы в MSDN:

public string CustomerName
{
    get
    {
        return this.customerNameValue;
    }
    set
    {
        if (value != this.customerNameValue)
        {
            this.customerNameValue = value;
            NotifyPropertyChanged();
        }
    }
}

Затем пользовательский интерфейс может быть обновлен из класса модели, а класс модели сможет обновляться в зависимости от изменений в пользовательском интерфейсе. Полный пример см. на связанной странице в MSDN.


Кроме того, вам не нужно объявлять ResourceDictionary в разделе Window.Resources... это это ResourceDictionary:

<Window.Resources>
    <local:ScenarioDataTableConverter x:Key="ScenarioDataTableConverter" />
</Window.Resources>
person Sheridan    schedule 06.01.2014
comment
INotifyPropertyChanged предназначен для обновления пользовательского интерфейса. Его проблема в том, что наоборот — обновление объектов из пользовательского интерфейса. - person Adi Lester; 07.01.2014
comment
ResourceDictionary — это пережиток упрощения; в реальном коде есть и другие вещи. Модель также представляет собой POCO из другой сборки. Я надеюсь, что мне не придется добавлять это в другую сборку, но можно было бы наследовать класс и реализовать интерфейс. - person Walter Williams; 07.01.2014