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 ListBox
exceto 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, SelectedValues
nã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 SelectedValues
dependendo 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.