WPF - ConvertBack для коллекции

256
20 июня 2017, 22:37

Имеется список всех контрагентов Contractors, имеется список партнеров Partners, второй список есть подмножество первого, оба свойства доступны в VM.

Во View есть ItemsControl с CheckBox'ами в котором содержится список контрагентов:

<ItemsControl ItemsSource="{Binding Contractors}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Name}">
                ...
            </CheckBox>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

мне нужно теперь расставить флажки в чекбоксах для всех контрагентов, которые являются партнерами:

                <CheckBox.IsChecked>
                    <MultiBinding Converter="{StaticResource CheckPartnersConverter}">
                        <Binding Mode="OneWay"/>
                        <Binding Path="DataContext.Partners"
                                 RelativeSource="{RelativeSource FindAncestor, AncestorType=ItemsControl}"/>
                    </MultiBinding>
                </CheckBox.IsChecked>

В конвертере реализовал метод Convert:

class CheckPartnersConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[1] is List<Contractor> partners && values[0] is Contractor c)
            return partners.Contains(c);
        return false;
    }

хорошо, это, вроде, работает.

Как теперь реализовать обратное конвертирование и вообще возможно ли его реализовать?

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        var res = new object[]
        {
            Binding.DoNothing,
            Binding.DoNothing
        };
        return res;
    }
}

Если это невозможно, готов отказаться от представления чекбоксами и заменить его, например на ListBox с SelectionMode="Multiple" и партнеров просто выделять, но там тоже проблемы с SelectedItems победить не удается

Answer 1

Мне кажется, вы усложняете себе жизнь.

Смотрите, ваша бизнес-логика хочет отобрать из контрагентов партнёров. Это существенный кусок бизнес-логики, а значит, имеет смысл не пожалеть завести для него отдельный VM-класс:

class MaybePartner : NotifyPropertyChangedImpl
{
    public Contractor Contractor { get; }
    bool isPartner;
    public bool IsPartner
    {
        get => isPartner;
        set
        {
            if (Set(ref isPartner, value))
            {
                // тут можно включить контрагента в список или исключить его
            }
        }
    }
    public MaybePartner(Contractor c, bool isInitiallyPartner)
    {
        Contractor = c;
        isPartner = isInitiallyPartner;
    }
}

Теперь ваша задача тривиальна: вы при входе в эту часть бизнес логики создаёте список MaybePartner'ов, и привязываетесь просто как

<ItemsControl ItemsSource="{Binding MaybePartners}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Contractor.Name}" IsChecked="{Binding IsPartner}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Вроде бы всё.

Answer 2

Ну тут на самом деле задачу можно решить несколькими способами, один из них это использовать EventTrigger, есть еще вариант с (attached property и behavior), но на самом деле суть этих решений примерно одна.

XAML код:

<ListBox Name="MyListBox"
             Grid.Row="0"
             DisplayMemberPath="Name"
             ItemsSource="{Binding Users}"
             SelectionMode="Multiple">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}"
                                       CommandParameter="{Binding ElementName=MyListBox, Path=SelectedItems}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
</ListBox>

Далее во ViewModel создаем команду:

public Command<IList> SelectedItemChangedCommand { get; set; }

В качестве параметров Action'a данной команды как-раз таки и будут выделенные записи

SelectedItemChangedCommand = new Command<IList>(items => 
{
    foreach (User item in items)
    {
    }
});

P.S. Для работы с EventTrigger и InvokeCommandAction у вас должна быть подключена библиотека System.Windows.Interactivity.

В XAML подключение данного пространства имен выглядит следующим образом:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Для того, что бы привязка к свойству SelectedItems работала в двух направлениях, т.е. что бы была возможность устанавливать данное свойство из VM, можно воспользоваться механизмом attached property. При его использовании EventTrigger уже не нужен.

public class SelectedItemsAttachedProperty
{
    public static IList GetSelectedItems(ListBox obj)
    {
        return (IList)obj.GetValue(SelectedItemsProperty);
    }
    public static void SetSelectedItems(ListBox obj, IList value)
    {
        obj.SetValue(SelectedItemsProperty, value);
    }
    public static readonly DependencyProperty
        SelectedItemsProperty =
            DependencyProperty.RegisterAttached(
                "SelectedItems",
                typeof(IList),
                typeof(SelectedItemsAttachedProperty),
                new PropertyMetadata(null,
                    SelectedItemsChanged));
    private static void SelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var lb = d as ListBox;
        if(lb == null)
            return;
        IList coll = e.NewValue as IList;
        if (coll is INotifyCollectionChanged)
        {
            (coll as INotifyCollectionChanged)
                .CollectionChanged += (s, collArgs) =>
                {
                    if (collArgs.OldItems != null)
                        foreach (var item in collArgs.OldItems)
                            lb.SelectedItems.Remove(item);
                    if (null != collArgs.NewItems)
                        foreach (var item in collArgs.NewItems)
                            lb.SelectedItems.Add(item);
                };
        }
        if (coll != null)
        {
            if (coll.Count > 0)
            {
                lb.SelectedItems.Clear();
                foreach (var item in coll)
                    lb.SelectedItems.Add(item);
            }
            lb.SelectionChanged += (s, lbArgs) =>
            {
                if (null != lbArgs.RemovedItems)
                    foreach (var item in lbArgs.RemovedItems)
                        coll.Remove(item);
                if (null != lbArgs.AddedItems)
                    foreach (var item in lbArgs.AddedItems)
                        coll.Add(item);
            };
        }
    }
}

XAML

<ListBox Grid.Row="0"
       wpfApp:SelectedItemsAttachedProperty.SelectedItems="{Binding SelectedItems}"
       DisplayMemberPath="Name"
       ItemsSource="{Binding Data}"
       SelectionMode="Multiple" />

Следовательно затем во VM вы можете добавлять элементы в SelectedItems, которое привязано к нашему AttachedProperty.

Answer 3

Переделал пока вот так:

<ListBox ItemsSource="{Binding Contractors}"
         DisplayMemberPath="Name" SelectionMode="Multiple"
         DataContextChanged="OnDataContextChanged"/>

Код обработчика события:

private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var listBox = sender as ListBox;
    if (e.OldValue is Provider oldProv)
        oldProv.Partners = listBox.SelectedItems.Cast<Contractor>().ToList();
    if (e.NewValue is Provider newProv)
    {
        listBox.SelectedItems.Clear();
        foreach (var c in newProv.Partners)
            listBox.SelectedItems.Add(c);
    }
}

В принципе всё работает и решение такое меня устраивает. Подсказал бы кто как его адаптировать к варианту представления с чекбоксами.

READ ALSO
Как узнать какие порты открыты на локальном компьютере?

Как узнать какие порты открыты на локальном компьютере?

Как узнать какие порты открыты на моем девайсе, программным методом C#

449
Контекстное меню в DataGrid - C#(WPF)

Контекстное меню в DataGrid - C#(WPF)

Пишу академическое десктоп приложение с базой данныхИз базы контент вывожу посредством DataGrid, которое находится в интерфейсе десктоп - клиенте

503
Отображение запроса в гриде

Отображение запроса в гриде

ЗдравствуйтеПоявилась проблема с отображением запроса в гриде

251
Электронная подпись XML

Электронная подпись XML

Не могу придти к общему пониманию механизмов подписи xml-документовБуду благодарен за помощь разобраться)

346