web-dev-qa-db-fra.com

Manière correcte d'utiliser CollectionViewSource dans ViewModel

J'ai utilisé un glisser-déposer pour lier un objet de source de données (un modèle de base de données) à DataGrid (en suivant essentiellement cet exemple dans Entité Framework Liaison de données avec WPF

Tout fonctionne bien avec cette implémentation.

XAML

<Window.Resources>    
<CollectionViewSource x:Key="categoryViewSource"  
    d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
</Window.Resources>
<Grid DataContext="{StaticResource categoryViewSource}">
..

Code derrière

private void Window_Loaded(object sender, RoutedEventArgs e)
{
   System.Windows.Data.CollectionViewSource categoryViewSource =
      ((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));

  _context.Categories.Load();
  categoryViewSource.Source = _context.Categories.Local;        
}

ViewModel

public MainWindow()
{
    InitializeComponent();
    this.DataContext = new MyViewModel();
}

Cependant, lorsque j'essaie d'utiliser le même code dans ViewModel, cela ne fonctionne pas (FindResource n'est pas disponible). De plus, je ne pense pas que ce soit la bonne approche (c'est-à-dire d'utiliser x:Key dans MVVM).

J'apprécierais vraiment toute aide pour m'indiquer quelle est la bonne façon de mettre en œuvre CollectionViewSource et DataBinding avec DataGrid.

37
David S

Vous avez deux options pour utiliser CollectionViewSource correctement avec MVVM -

  1. Exposez une ObservableCollection éléments (Categories dans votre cas) via votre ViewModel et créez CollectionViewSource en XAML comme ceci -

    <CollectionViewSource Source="{Binding Path=Categories}">
        <CollectionViewSource.SortDescriptions>
           <scm:SortDescription PropertyName="CategoryName" />
        </CollectionViewSource.SortDescriptions>
    </CollectionViewSource>
    

    scm: xmlns:scm="clr-namespace:System.ComponentModel;Assembly=Wind‌​owsBase"

    voir ceci - Filtering collections de XAML en utilisant CollectionViewSource

  2. Créez et exposez une ICollectionView directement à partir de votre ViewModel

    see this - Comment naviguer, grouper, trier et filtrer des données dans WPF

L'exemple suivant montre comment créer une vue de collection et que Le lier à une ListBox

Voir XAML:

<Window 
    x:Class="CustomerView"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml">
    <ListBox ItemsSource={Binding Customers} />
</Window>

Voir Codebehind:

public class CustomerView
{
   public CustomerView()
   {
        DataContext = new CustomerViewModel();
   }
}

ViewModel:

public class CustomerViewModel
{
    private ICollectionView _customerView;

    public ICollectionView Customers
    {
        get { return _customerView; }
    }

    public CustomerViewModel()
    {
        IList<Customer> customers = GetCustomers();
        _customerView = CollectionViewSource.GetDefaultView(customers);
    }
}
50
akjoshi

J'ai trouvé qu'il était pratique d'avoir une CollectionViewSource dans mon ViewModel et de lier la ListBox (dans mon cas) au CollectionViewSource.View tout en définissant le CollectionViewSource.Source comme étant la liste que je veux utiliser. 

Ainsi:

ViewModel: 

    public DesignTimeVM()  //I'm using this as a Design Time VM 
    {
        Items = new List<Foo>();
        Items.Add(new Foo() { FooProp= "1", FooPrep= 20.0 });
        Items.Add(new Foo() { FooProp= "2", FooPrep= 30.0 });

        FooViewSource = new CollectionViewSource();
        FooViewSource.Source = Items;

        SelectedFoo = Items.First();

        //More code as needed
    }

XAML: 

<ListBox ItemsSource="{Binding FooViewSource.View}" SelectedItem="{Binding SelectedFoo}"/>

Cela signifie que je peux faire des choses intéressantes dans la VM si nécessaire (à partir de https://blogs.msdn.Microsoft.com/matt/2008/08/28/collectionview-deferrefresh-my-new-best- ami/ ):

        using (FooViewSource.DeferRefresh())
        {
            //Remove an old Item
            //add New Item
            //sort list anew, etc. 
        }

Je suppose que cela est également possible avec l’utilisation de l’objet ICollectionView, mais le code de démonstration dans le lien du blog semble être quelque chose de codebehind, renvoyant directement à la zone de liste, ce que j’essaie d’éviter. 

BTW avant de demander, voici comment utiliser un ordinateur virtuel au moment du design: Modèle de vue au moment du design de WPF

6
gakera

À titre de référence, une autre méthode consiste à utiliser une propriété attachée sur CollectionViewSource, qui dirige ensuite les fonctions vers ViewModel (implémentation d’une interface).

Ceci est une démonstration très basique juste pour le filtrage, il faudrait du travail pour, par exemple. une deuxième collection sur le VM mais je pense que cela suffit pour montrer la technique générale.

Si cela est meilleur ou pire que les autres méthodes est en discussion, je voulais juste souligner, qu'il y a une autre façon de faire

Définition de la propriété attachée:

public static class CollectionViewSourceFilter
{
    public static IFilterCollectionViewSource GetFilterObject(CollectionViewSource obj)
    {
        return (IFilterCollectionViewSource)obj.GetValue(FilterObjectProperty);
    }

    public static void SetFilterObject(CollectionViewSource obj, IFilterCollectionViewSource value)
    {
        obj.SetValue(FilterObjectProperty, value);
    }

    public static void FilterObjectChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.OldValue is IFilterCollectionViewSource oldFilterObject
            && sender is CollectionViewSource oldCvs)
        {
            oldCvs.Filter -= oldFilterObject.Filter;
            oldFilterObject.FilterRefresh -= (s, e2) => oldCvs.View.Refresh();
        }

        if (e.NewValue is IFilterCollectionViewSource filterObject
            && sender is CollectionViewSource cvs)
        {
            cvs.Filter += filterObject.Filter;
            filterObject.FilterRefresh += (s,e2) => cvs.View.Refresh();
        }
    }

    public static readonly DependencyProperty FilterObjectProperty = DependencyProperty.RegisterAttached(
        "FilterObject",
        typeof(Interfaces.IFilterCollectionViewSource),
        typeof(CollectionViewSourceFilter),
        new PropertyMetadata(null,FilterObjectChanged)
    );
}

Interface:

public interface IFilterCollectionViewSource
{
    void Filter(object sender, FilterEventArgs e);
    event EventHandler FilterRefresh;
}

utilisation en xaml:

<CollectionViewSource
        x:Key="yourKey"
        Source="{Binding YourCollection}"
        classes:CollectionViewSourceFilter.FilterObject="{Binding}" />

et utilisation dans le ViewModel:

class YourViewModel : IFilterCollectionViewSource
{
    public event EventHandler FilterRefresh;

    private string _SearchTerm = string.Empty;
    public string SearchTerm
    {
        get { return _SearchTerm; }
        set {
            SetProperty(ref _SearchTerm, value);
            FilterRefresh?.Invoke(this, null);
        }
    }

    private ObservableCollection<YourItemType> _YourCollection = new ObservableCollection<YourItemType>();
    public ObservableCollection<YourItemType> YourCollection
    {
        get { return _YourCollection; }
        set { SetProperty(ref _YourCollection, value); }
    }

    public void Filter(object sender, FilterEventArgs e)
    {
        e.Accepted = (e.Item as YourItemType)?.YourProperty?.ToLower().Contains(SearchTerm.ToLower()) ?? true;
    }
}
0
FastJack