wpf - Filtro desencadenante en CollectionViewSource
xaml mvvm (6)
Estoy trabajando en una aplicación de escritorio WPF usando el patrón MVVM.
Estoy tratando de filtrar algunos elementos de un ListView
basado en el texto escrito en un TextBox
. Quiero que los elementos de ListView
se filtren a medida que cambio el texto.
Quiero saber cómo activar el filtro cuando cambia el texto del filtro.
ListView
une a un CollectionViewSource
, que se une a ObservableCollection
en mi ViewModel. El TextBox
para el texto del filtro se une a una cadena en el modelo de vista, con UpdateSourceTrigger=PropertyChanged
, como debería ser.
<CollectionViewSource x:Key="ProjectsCollection"
Source="{Binding Path=AllProjects}"
Filter="CollectionViewSource_Filter" />
<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
<ListView DataContext="{StaticResource ProjectsCollection}"
ItemsSource="{Binding}" />
Filter="CollectionViewSource_Filter"
vincula a un controlador de eventos en el código subyacente, que simplemente llama a un método de filtro en ViewModel.
El filtrado se realiza cuando el valor de FilterText cambia: el colocador de la propiedad FilterText llama a un método FilterList que itera sobre el ObservableCollection
en mi ViewModel y establece una propiedad Boolean FilteredOut en cada elemento ViewModel.
Sé que la propiedad FilteredOut se actualiza cuando cambia el texto del filtro, pero la Lista no se actualiza. El evento de filtro CollectionViewSource
solo se activa cuando recargo el UserControl alejándolo de él y viceversa.
Intenté llamar a OnPropertyChanged("AllProjects")
después de actualizar la información del filtro, pero no resolvió mi problema. ("AllProjects" es la propiedad ObservableCollection
en mi ViewModel al que se vincula el CollectionViewSource
).
¿Cómo puedo hacer que CollectionViewSource
vuelva a filtrar cuando cambia el valor de FilterText TextBox
?
Muchas gracias
Acabo de descubrir una solución mucho más elegante para este problema. En lugar de crear un ICollectionView
en su ViewModel (como lo sugiere la respuesta aceptada) y establecer su enlace a
ItemsSource={Binding Path=YourCollectionViewSourceProperty}
La mejor manera es crear una propiedad CollectionViewSource
en su ViewModel. A continuación, vincula tu ItemsSource
siguiente manera
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}
Observe la adición de .View. De esta forma, el enlace de ItemsSource
se notifica cada vez que hay un cambio en CollectionViewSource
y nunca tiene que llamar manualmente a Refresh()
en ICollectionView
Nota: No puedo determinar por qué este es el caso. Si se vincula directamente a una propiedad CollectionViewSource
, el enlace falla. Sin embargo, si define un CollectionViewSource
en su elemento Resources
de un archivo XAML y se vincula directamente a la clave de recurso, el enlace funciona bien. Lo único que puedo adivinar es que cuando lo haces completamente en XAML, sabes que realmente quieres vincularlo al valor CollectionViewSource.View y lo vincula para que estés detrás de escena (¡qué útil!: /).
Hoy en día, a menudo no necesita activar actualizaciones de forma explícita. CollectionViewSource
implementa ICollectionViewLiveShaping
que se actualiza automáticamente si IsLiveFilteringRequested
es verdadero, en función de los campos de su colección LiveFilteringProperties
.
Un ejemplo en XAML:
<CollectionViewSource
Source="{Binding Items}"
Filter="FilterPredicateFunction"
IsLiveFilteringRequested="True">
<CollectionViewSource.LiveFilteringProperties>
<system:String>FilteredProperty1</system:String>
<system:String>FilteredProperty2</system:String>
</CollectionViewSource.LiveFilteringProperties>
</CollectionViewSource>
No cree un CollectionViewSource
en su vista. En su lugar, cree una propiedad de tipo ICollectionView
en su modelo de vista y vincule ListView.ItemsSource
a este.
Una vez que hayas hecho esto, puedes poner la lógica en el FilterText
propiedad FilterText
que llama a Refresh()
en el ICollectionView
cada vez que el usuario lo cambie.
Descubrirá que esto también simplifica el problema de la clasificación: puede construir la lógica de clasificación en el modelo de vista y luego exponer los comandos que la vista puede usar.
EDITAR
Aquí hay una demostración bastante simple de clasificación dinámica y filtrado de una vista de colección usando MVVM. Esta demostración no implementa FilterText
, pero una vez que comprenda cómo funciona, no debería tener ninguna dificultad para implementar una propiedad FilterText
y un predicado que use esa propiedad en lugar del filtro codificado que está usando ahora.
(Tenga en cuenta también que las clases de modelo de vista aquí no implementan notificaciones de cambio de propiedad. Eso es solo para mantener el código simple: como nada en esta demostración realmente cambia los valores de propiedad, no necesita notificación de cambio de propiedad).
Primero una clase para tus artículos:
public class ItemViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
Ahora, un modelo de vista para la aplicación. Aquí están sucediendo tres cosas: primero, crea y completa su propia ICollectionView
; segundo, expone un ApplicationCommand
(ver abajo) que la vista usará para ejecutar los comandos de clasificación y filtrado, y finalmente, implementa un método Execute
que ordena o filtra la vista:
public class ApplicationViewModel
{
public ApplicationViewModel()
{
Items.Add(new ItemViewModel { Name = "John", Age = 18} );
Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });
ItemsView = CollectionViewSource.GetDefaultView(Items);
}
public ApplicationCommand ApplicationCommand
{
get { return new ApplicationCommand(this); }
}
private ObservableCollection<ItemViewModel> Items =
new ObservableCollection<ItemViewModel>();
public ICollectionView ItemsView { get; set; }
public void ExecuteCommand(string command)
{
ListCollectionView list = (ListCollectionView) ItemsView;
switch (command)
{
case "SortByName":
list.CustomSort = new ItemSorter("Name") ;
return;
case "SortByAge":
list.CustomSort = new ItemSorter("Age");
return;
case "ApplyFilter":
list.Filter = new Predicate<object>(x =>
((ItemViewModel)x).Age > 21);
return;
case "RemoveFilter":
list.Filter = null;
return;
default:
return;
}
}
}
La clasificación es una mierda; necesitas implementar un IComparer
:
public class ItemSorter : IComparer
{
private string PropertyName { get; set; }
public ItemSorter(string propertyName)
{
PropertyName = propertyName;
}
public int Compare(object x, object y)
{
ItemViewModel ix = (ItemViewModel) x;
ItemViewModel iy = (ItemViewModel) y;
switch(PropertyName)
{
case "Name":
return string.Compare(ix.Name, iy.Name);
case "Age":
if (ix.Age > iy.Age) return 1;
if (iy.Age > ix.Age) return -1;
return 0;
default:
throw new InvalidOperationException("Cannot sort by " +
PropertyName);
}
}
}
Para activar el método Execute
en el modelo de vista, utiliza una clase ApplicationCommand
, que es una implementación simple de ICommand
que enruta el CommandParameter
en los botones de la vista para el método Execute
del modelo de visualización. Lo implementé de esta manera porque no quería crear un conjunto de propiedades de RelayCommand
en el modelo de vista de la aplicación, y quería mantener todo el ordenamiento / filtrado en un método para que fuera fácil ver cómo se hacía.
public class ApplicationCommand : ICommand
{
private ApplicationViewModel _ApplicationViewModel;
public ApplicationCommand(ApplicationViewModel avm)
{
_ApplicationViewModel = avm;
}
public void Execute(object parameter)
{
_ApplicationViewModel.ExecuteCommand(parameter.ToString());
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Finalmente, aquí está la MainWindow
para la aplicación:
<Window x:Class="CollectionViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<CollectionViewDemo:ApplicationViewModel />
</Window.DataContext>
<DockPanel>
<ListView ItemsSource="{Binding ItemsView}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}"
Header="Name" />
<GridViewColumn DisplayMemberBinding="{Binding Age}"
Header="Age"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel DockPanel.Dock="Right">
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByName">Sort by name</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByAge">Sort by age</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="ApplyFilter">Apply filter</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="RemoveFilter">Remove filter</Button>
</StackPanel>
</DockPanel>
</Window>
Si entendí bien lo que estás preguntando:
En la parte configurada de su propiedad FilterText
solo llame a Refresh()
a su CollectionView
.
Tal vez haya simplificado su Vista en su pregunta, pero tal como está escrita, realmente no necesita un CollectionViewSource: puede enlazar a una lista filtrada directamente en su ViewModel (mItemsToFilter es la colección que se está filtrando, probablemente "AllProjects" en tu ejemplo):
public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
get
{
if (String.IsNullOrEmpty(mFilterText))
return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);
var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
return new ReadOnlyObservableCollection<ItemsToFilter>(
new ObservableCollection<ItemsToFilter>(filtered));
}
}
public string FilterText
{
get { return mFilterText; }
set
{
mFilterText = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
}
}
}
Su vista simplemente sería:
<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />
Algunas notas rápidas:
Esto elimina el evento en el código detrás
También elimina la propiedad "FilterOut", que es una propiedad artificial, solo GUI y por lo tanto realmente rompe MVVM. A menos que planee serializar esto, no lo quisiera en mi ViewModel, y ciertamente no en mi Modelo.
En mi ejemplo, utilizo un "filtro de entrada" en lugar de un "filtro de salida". Me parece más lógico (en la mayoría de los casos) que el filtro que estoy aplicando sea lo que quiero ver. Si realmente quiere filtrar cosas, solo niegue la cláusula Contiene (es decir, item =>! Item.Text.Contains (...)).
Puede tener una forma más centralizada de hacer sus Sets en su ViewModel. Lo importante que debe recordar es que cuando cambie FilterText, también debe notificar a su colección AllFilteredItems. Lo hice en línea aquí, pero también podría manejar el evento PropertyChanged y llamar a PropertyChanged cuando e.PropertyName es FilterText.
Por favor, avíseme si necesita alguna aclaración.
CollectionViewSource.View.Refresh();
¡CollectionViewSource.Filter es reevaluado de esta manera!