Posted in

WPF ListBox SelectedItems Twoway Binding • oleksii holub

WPF ListBox SelectedItems Twoway Binding • oleksii holub

Por algumas razões pouco claras, WPF’s ListBox O controle não permite a ligação bidirecional no SelectedItems propriedade do jeito que faz com SelectedItem. Isso poderia ter sido muito útil ao usar o Multi-Select para vincular toda a lista de itens selecionados ao modelo de exibição.

Curiosamente, você ainda pode ligar Add()Assim, Remove()Assim, Clear() Métodos em ListBox.SelectedItems que atualiza a seleção corretamente, portanto, tudo se resume a implementar um comportamento que torna a propriedade vinculável.

Implementação de comportamento

Aqui está o comportamento que permite a ligação bidirecional SelectedItems:

public class ListBoxSelectionBehavior<T> : Behavior<ListBox>
{
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register(
            nameof(SelectedItems),
            typeof(IList),
            typeof(ListBoxSelectionBehavior),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnSelectedItemsChanged
            )
        );

    private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var behavior = (ListBoxSelectionBehavior) sender;
        if (behavior._modelHandled) return;

        if (behavior.AssociatedObject == null)
            return;

        behavior._modelHandled = true;
        behavior.SelectItems();
        behavior._modelHandled = false;
    }

    private bool _viewHandled;
    private bool _modelHandled;

    public IList SelectedItems
    {
        get => (IList) GetValue(SelectedItemsProperty);
        set => SetValue(SelectedItemsProperty, value);
    }

    // Propagate selected items from model to view
    private void SelectItems()
    {
        _viewHandled = true;
        AssociatedObject.SelectedItems.Clear();
        if (SelectedItems != null)
        {
            foreach (var item in SelectedItems)
                AssociatedObject.SelectedItems.Add(item);
        }
        _viewHandled = false;
    }

    // Propagate selected items from view to model
    private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args)
    {
        if (_viewHandled) return;
        if (AssociatedObject.Items.SourceCollection == null) return;

        SelectedItems = AssociatedObject.SelectedItems.Cast<T>().ToArray();
    }

    // Re-select items when the set of items changes
    private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        if (_viewHandled) return;
        if (AssociatedObject.Items.SourceCollection == null) return;

        SelectItems();
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
        ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged;
    }

    /// <inheritdoc />
    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (AssociatedObject != null)
        {
            AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
            ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged;
        }
    }
}

O comportamento acima define o seu próprio SelectedItems propriedade, idêntica àquele em ListBoxexceto que pode ser vinculado e não é somente leitura.

Quando a propriedade é alterada do modelo de visualização, o OnSelectedItemsChanged(...) O método é chamado, que é onde as alterações são propagadas para a visualização. Fazemos isso no SelectItems() método em que apenas limpamos e adicionamos novos itens ao ListBox.SelectedItems coleção.

Quando a alteração é acionada pela visão, chamamos o OnListBoxSelectionChanged(...) método. Para atualizar os itens selecionados no modelo de exibição, copiamos os itens de ListBox.SelectedItems para o nosso SelectedItems coleção.

Observe que esse comportamento é genérico porque esperamos ser capazes de se ligar a uma coleção de um tipo arbitrário no lado do modelo de exibição. O WPF não suporta comportamentos genéricos; no entanto, precisamos subtype esta classe para cada tipo de dados específico:

public class MyObjectListBoxSelectionBehavior : ListBoxSelectionBehavior<MyObject>
{
}

Uso

Agora podemos usar esse comportamento inicializando -o em XAML, assim:

<ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple">
    <i:Interaction.Behaviors>
        <behaviors:MyObjectListBoxSelectionBehavior SelectedItems="{Binding SelectedItems}" />
    </i:Interaction.Behaviors>
    <ListBox.ItemTemplate>
        <!-- ... -->
    </ListBox.ItemTemplate>
</ListBox>

Adicionando suporte ao SelectedValuepath

Outra característica útil de ListBox é que você pode fazer um proxy vinculativo usando SelectedValuePath e SelectedValue. Contexto SelectedValuePath permite especificar um caminho de membro a ser avaliado por SelectedValue.

A grande parte sobre isso é que também funciona ao contrário – mudando SelectedValue usará o caminho do membro em SelectedValuePath para atualizar SelectedItem com uma nova referência.

Isso também pode ser muito útil para multi-seleção, mas infelizmente a versão plural, SelectedValuesnão existe. Vamos estender nosso comportamento para adicionar suporte a ele.

public class ListBoxSelectionBehavior<T> : Behavior<ListBox>
{
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register(
            nameof(SelectedItems),
            typeof(IList),
            typeof(ListBoxSelectionBehavior),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnSelectedItemsChanged
            )
        );

    public static readonly DependencyProperty SelectedValuesProperty =
        DependencyProperty.Register(
            nameof(SelectedValues),
            typeof(IList),
            typeof(ListBoxSelectionBehavior),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnSelectedValuesChanged
            )
        );

    private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var behavior = (ListBoxSelectionBehavior) sender;
        if (behavior._modelHandled) return;

        if (behavior.AssociatedObject == null)
            return;

        behavior._modelHandled = true;
        behavior.SelectedItemsToValues();
        behavior.SelectItems();
        behavior._modelHandled = false;
    }

    private static void OnSelectedValuesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        var behavior = (ListBoxSelectionBehavior) sender;
        if (behavior._modelHandled) return;

        if (behavior.AssociatedObject == null)
            return;

        behavior._modelHandled = true;
        behavior.SelectedValuesToItems();
        behavior.SelectItems();
        behavior._modelHandled = false;
    }

    private static object GetDeepPropertyValue(object obj, string path)
    {
        if (string.IsNullOrWhiteSpace(path)) return obj;
        while (true)
        {
            if (path.Contains('.'))
            {
                string() split = path.Split('.');
                string remainingProperty = path.Substring(path.IndexOf('.') + 1);
                obj = obj.GetType().GetProperty(split(0)).GetValue(obj, null);
                path = remainingProperty;
                continue;
            }
            return obj.GetType().GetProperty(path).GetValue(obj, null);
        }
    }

    private bool _viewHandled;
    private bool _modelHandled;

    public IList SelectedItems
    {
        get => (IList) GetValue(SelectedItemsProperty);
        set => SetValue(SelectedItemsProperty, value);
    }

    public IList SelectedValues
    {
        get => (IList) GetValue(SelectedValuesProperty);
        set => SetValue(SelectedValuesProperty, value);
    }

    // Propagate selected items from model to view
    private void SelectItems()
    {
        _viewHandled = true;
        AssociatedObject.SelectedItems.Clear();
        if (SelectedItems != null)
        {
            foreach (var item in SelectedItems)
                AssociatedObject.SelectedItems.Add(item);
        }
        _viewHandled = false;
    }

    // Update SelectedItems based on SelectedValues
    private void SelectedValuesToItems()
    {
        if (SelectedValues == null)
        {
            SelectedItems = null;
        }
        else
        {
            SelectedItems =
                AssociatedObject.Items.Cast<T>()
                    .Where(i => SelectedValues.Contains(GetDeepPropertyValue(i, AssociatedObject.SelectedValuePath)))
                    .ToArray();
        }
    }

    // Update SelectedValues based on SelectedItems
    private void SelectedItemsToValues()
    {
        if (SelectedItems == null)
        {
            SelectedValues = null;
        }
        else
        {
            SelectedValues =
                SelectedItems.Cast<T>()
                    .Select(i => GetDeepPropertyValue(i, AssociatedObject.SelectedValuePath))
                    .ToArray();
        }
    }

    // Propagate selected items from view to model
    private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args)
    {
        if (_viewHandled) return;
        if (AssociatedObject.Items.SourceCollection == null) return;

        SelectedItems = AssociatedObject.SelectedItems.Cast<object>().ToArray();
    }

    // Re-select items when the set of items changes
    private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        if (_viewHandled) return;
        if (AssociatedObject.Items.SourceCollection == null) return;

        SelectItems();
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.SelectionChanged += OnListBoxSelectionChanged;
        ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged;

        _modelHandled = true;
        SelectedValuesToItems();
        SelectItems();
        _modelHandled = false;
    }

    /// <inheritdoc />
    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (AssociatedObject != null)
        {
            AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged;
            ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged;
        }
    }
}

Eu adicionei outra propriedade de dependência para SelectedValues e alguns novos métodos.

SelectedValuesToItems() e SelectedItemsToValues() converter entre SelectedItems e SelectedValuesdependendo de qual propriedade foi atualizada. GetDeepPropertyValue(...) é usado para extrair o valor da propriedade usando um objeto e um caminho de membro, para estabelecer conformidade entre itens selecionados e seus valores.

Uso com o SelectedValuepath

Agora podemos especificar SelectedValuePath em ListBox e nosso comportamento nos permitirá vincular o SelectedValues propriedade para o modelo e vice -versa.

<ListBox ItemsSource="{Binding Items}" SelectedValuePath="ID" SelectionMode="Multiple">
    <i:Interaction.Behaviors>
        <behaviors:MyObjectListBoxSelectionBehavior SelectedValues="{Binding SelectedValues}" />
    </i:Interaction.Behaviors>
    <ListBox.ItemTemplate>
        <!-- ... -->
    </ListBox.ItemTemplate>
</ListBox>

Luis es un experto en Inteligência Empresarial, Redes de Computadores, Gestão de Dados e Desenvolvimento de Software. Con amplia experiencia en tecnología, su objetivo es compartir conocimientos prácticos para ayudar a los lectores a entender y aprovechar estas áreas digitales clave.

Leave a Reply

Your email address will not be published. Required fields are marked *

77f