Walter Williams
Walter Williams

Reputation: 974

WPF IValueConverter.ConvertBack not called

I have some code below which is a simplified example of what I'm trying to do. I'm using a converter to try to fill a DataGrid with data from a model that I have. The DataGrid is being populated correctly, but any changes in the grid are not being persisted back to the objects. I have specified the mode as TwoWay. When I put a breakpoint on the converters ConvertBack method, it is never being called.

I am fairly new to WPF and MVVM, so I don't see what I'm doing wrong. There is not much I can do to change the model, so I'd like to see if this can work unless there is a clearly superior method.

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>

App.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();
        }
    }
}

Upvotes: 1

Views: 2433

Answers (2)

Adi Lester
Adi Lester

Reputation: 25201

Converters don't work that way when it comes to collections. ConvertBack will only be called when the entire collection is replaced. It won't be called when an item in the collection is modified. In your case the collection (the DataView) isn't being replaced with a new DataView instance, but rather modified, and that's why ConvertBack isn't being called.

If you ask me, I don't see why you need to use the converter anyway. Either bind directly to the Scenarios property and work on this collection that is exposed by the view model, or alternatively call the conversion code in your viewmodel and expose the resulting DataView in a different property. Then you'll just need to bind to that property without specifying a converter.

Upvotes: 2

Sheridan
Sheridan

Reputation: 69959

This sounds like the classic beginner's mistake... I think that you need to implement the INotifyPropertyChanged Interface in your model classes. The idea is that you inform the INotifyPropertyChanged interface when any property value changes. From the linked page on MSDN:

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

The UI can then be updated from the model class and the model class will be able to be updated from changes in the UI. See the linked page on MSDN for the full example.


Also, you don't need to declare a ResourceDictionary in the Window.Resources section... it is a ResourceDictionary:

<Window.Resources>
    <local:ScenarioDataTableConverter x:Key="ScenarioDataTableConverter" />
</Window.Resources>

Upvotes: -1

Related Questions