WPF DataGrid - объединение TimeSeries с MultiBinding, уведомление об изменении потери. Зачем? - PullRequest
3 голосов
/ 18 мая 2009

У меня есть класс, который имеет два ObservableCollection , где TimeValue - это пользовательская пара DateTime / Value с уведомлением об изменении (через INotifyPropertyChanged). Я называю эти цели и факты.

Когда я связываю их с диаграммой, все работает отлично, и я получаю два LineSeries. Если я связываю один из них с DataGrid, со столбцом для «Дата» и столбцом для «Значение», снова работает отлично. Я даже получаю привязку TwoWay, которая мне нужна.

Однако мне нужно иметь DataGrid со столбцом «Дата» и столбцом для целей и фактов. Проблема в том, что мне нужно перечислить ВСЕ даты в диапазоне, в то время как некоторые из этих дат могут не иметь соответствующих значений в целях, фактах или в обоих.

Итак, я решил, что я сделаю MultiBinding, который будет принимать Targets и Actuals в качестве входных данных и выводить комбинированный TimeSeriesC с нулевыми значениями всякий раз, когда один из оригиналов не имеет значения.

Работает, но не реагирует на изменения в базовых данных.

Это прекрасно работает (привязка к одной ObservableCollection):

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
    <Binding Path="Targets"/>
    <!--<MultiBinding Converter="{StaticResource TargetActualListConverter}">
        <Binding Path="Targets"/>
        <Binding Path="Actuals"/>
    </MultiBinding>-->
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>-->
</ctrls:DataGrid.Columns>

Это работает, но только при первой инициализации. Нет ответа на уведомление об изменении:

<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
    <!--<Binding Path="Targets"/>-->
    <MultiBinding Converter="{StaticResource TargetActualListConverter}">
        <Binding Path="Targets"/>
        <Binding Path="Actuals"/>
    </MultiBinding>
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
    <ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
    <!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>-->
    <ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
    <ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>
</ctrls:DataGrid.Columns>

А вот мой IMultiValueConverter:

class TargetActualListConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        TimeSeries<double> Targets = values[0] as TimeSeries<double>;
        TimeSeries<double> Actuals = values[1] as TimeSeries<double>;
        DateTime[] range = TimeSeries<double>.GetDateRange(Targets, Actuals);//Get min and max Dates
        int count = (range[1] - range[0]).Days;//total number of days
        DateTime currDate = new DateTime();
        TimeSeries<double?[]> combined = new TimeSeries<double?[]>();
        for (int i = 0; i < count; i++)
        {
            currDate = range[0].AddDays(i);
            double?[] vals = { Targets.Dates.Contains(currDate) ? (double?)Targets.GetValueByDate(currDate) : null, Actuals.Dates.Contains(currDate) ? (double?)Actuals.GetValueByDate(currDate) : null };
            combined.Add(new TimeValue<double?[]>(currDate, vals));
        }
        return combined;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        TimeSeries<double?[]> combined = value as TimeSeries<double?[]>;
        TimeSeries<double> Targets = new TimeSeries<double>();
        TimeSeries<double> Actuals = new TimeSeries<double>();

        foreach (TimeValue<double?[]> tv in combined)
        {
            if(tv.Value[0]!=null)
                Targets.Add(new TimeValue<double>(tv.Date,(double)tv.Value[0]));
            if (tv.Value[1] != null)
                Actuals.Add(new TimeValue<double>(tv.Date, (double)tv.Value[1]));
        }
        TimeSeries<double>[] result = { Targets, Actuals };
        return result;

    }
}

Я не могу быть слишком далеко, так как он отображает значения.

Что я делаю не так? Или, альтернативно, есть более простой способ сделать это?

Спасибо всем!

1 Ответ

2 голосов
/ 19 мая 2009

Похоже, это вызвано конвертером. ObservableCollection реализует INotifyCollectionChanged, который уведомляет пользовательский интерфейс об изменении коллекции (Добавить / Удалить / Заменить / Переместить / Сбросить). Это все изменения в коллекции, а не ее содержимое, поэтому обновления, которые вы видели раньше, были связаны с тем, что ваш класс реализовал INotifyPropertyChanged. Поскольку MultiCoverter возвращает новую коллекцию новых объектов, данные в исходных коллекциях не будут распространяться на них, так как нет привязок к исходным объектам, которые они могли бы уведомить.

Первое, что я хотел бы предложить, - взглянуть на элемент CompositeCollection и посмотреть, подойдет ли он вашим потребностям.

Вместо того, чтобы устанавливать ItemsSource таким, какой вы есть, вы могли бы поддерживать исходные объекты примерно так:

<ctrls:DataGrid.ItemsSource>
    <CompositeCollection>
            <CollectionContainer Collection="{Binding Targets}" />
            <CollectionContainer Collection="{Binding Actuals}" />
       </CompositeCollection>
</ctrls:DataGrid.ItemsSource>

(я предполагаю, что «не реагирует на какие-либо изменения в базовых данных» относится к изменению значений, а не к изменению коллекции, если я ошибаюсь, дайте мне знать, и я более подробно на это посмотрю). )

Редактировать дополнения
В случае, если это не работает, альтернативой является написание нового класса, который обернет как целевые, так и фактические коллекции. Затем с помощью этих оболочек можно создать одну коллекцию ObservableCollection. На самом деле это лучший метод по сравнению с использованием ValueConverter или CompositeCollection. В любом случае вы потеряете часть функциональности, которая была изначально. Используя преобразователь значений для воссоздания коллекции, она больше не привязывается напрямую к исходным объектам, поэтому уведомление о свойстве может быть потеряно. При использовании CompositeCollection у вас больше нет отдельной коллекции, которую можно перебирать или изменять с помощью добавления / удаления / перемещения и т. Д., Поскольку он должен знать, над какой коллекцией работать.

Этот тип функциональности обертывания может быть весьма полезен в WPF и является очень упрощенной версией ViewModel, являющейся частью шаблона проектирования M-V-VM. Его можно использовать, когда у вас нет доступа к базовым классам для добавления INotifyPropertyChanged или IDataErrorInfo, а также может помочь добавить дополнительные функции, такие как состояние и взаимодействие, к базовым моделям.

Вот краткий пример, демонстрирующий эту функциональность, когда оба наших начальных класса имеют одно и то же свойство Name и не реализуют INotifyPropertyChanged, который не разделяется между ними.

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        Foo foo1 = new Foo { ID = 1, Name = "Foo1" };
        Foo foo3 = new Foo { ID = 3, Name = "Foo3" };
        Foo foo5 = new Foo { ID = 5, Name = "Foo5" };
        Bar bar1 = new Bar { ID = 1, Name = "Bar1" };
        Bar bar2 = new Bar { ID = 2, Name = "Bar2" };
        Bar bar4 = new Bar { ID = 4, Name = "Bar4" };

        ObservableCollection<FooBarViewModel> fooBar = new ObservableCollection<FooBarViewModel>();
        fooBar.Add(new FooBarViewModel(foo1, bar1));
        fooBar.Add(new FooBarViewModel(bar2));
        fooBar.Add(new FooBarViewModel(foo3));
        fooBar.Add(new FooBarViewModel(bar4));
        fooBar.Add(new FooBarViewModel(foo5));

        this.DataContext = fooBar;
    }
}

public class Foo
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class Bar
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class FooBarViewModel : INotifyPropertyChanged
{
    public Foo WrappedFoo { get; private set; }
    public Bar WrappedBar { get; private set; }

    public int ID
    {
        get
        {
            if (WrappedFoo != null)
            { return WrappedFoo.ID; }
            else if (WrappedBar != null)
            { return WrappedBar.ID; }
            else
            { return -1; }
        }
        set
        {
            if (WrappedFoo != null)
            { WrappedFoo.ID = value; }
            if (WrappedBar != null)
            { WrappedBar.ID = value; }

            this.NotifyPropertyChanged("ID");
        }
    }

    public string BarName
    {
        get
        {
            return WrappedBar.Name;
        }
        set
        {
            WrappedBar.Name = value;
            this.NotifyPropertyChanged("BarName");
        }
    }

    public string FooName
    {
        get
        {
            return WrappedFoo.Name;
        }
        set
        {
            WrappedFoo.Name = value;
            this.NotifyPropertyChanged("FooName");
        }
    }

    public FooBarViewModel(Foo foo)
        : this(foo, null) { }
    public FooBarViewModel(Bar bar)
        : this(null, bar) { }
    public FooBarViewModel(Foo foo, Bar bar)
    {
        WrappedFoo = foo;
        WrappedBar = bar;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

А потом в окне:

 <ListView ItemsSource="{Binding}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
            <GridViewColumn Header="Foo Name" DisplayMemberBinding="{Binding FooName}"/>
            <GridViewColumn Header="Bar Name" DisplayMemberBinding="{Binding BarName}"/>
        </GridView>
    </ListView.View>
</ListView>
...