web-dev-qa-db-fra.com

Implémentation de WPF MVVM INotifyPropertyChanged - Model ou ViewModel

J'ai lu un certain nombre de débats sur où implémenter INotifyPropertyChanged ici sur StackOverflow et d'autres blogs mais il semble qu'il y ait des cas où vous devez l'implémenter sur le modèle. Voici mon scénario - je recherche des commentaires sur ma conclusion ou mon approche est fausse.

J'utilise cette implémentation d'un ObservableDictionary ( ObservableDictionary ) car j'ai besoin de requêtes performantes à l'aide de la clé.

Dans ce dictionnaire, je place la collection d'objets Model.

Dans ma machine virtuelle, je déclare une instance (Books) du dictionnaire et dans la liaison XAML à celui-ci.

    <tk:DataGrid AutoGenerateColumns="False" Grid.Row="1" ItemsSource="{Binding Mode=TwoWay, Path=Books.Store}" Grid.ColumnSpan="2" Margin="3">
        <tk:DataGrid.Columns>
            <tk:DataGridTextColumn Binding="{Binding Mode=TwoWay, Path=Value.Name}" MinWidth="100" Header="Name" />
            <tk:DataGridTextColumn Binding="{Binding Mode=TwoWay, Path=Value.Details}" MinWidth="300" Header="Details" />
        </tk:DataGrid.Columns>        
    </tk:DataGrid>  

Si j'implémente INotifyPropertyChanged sur le VM pour les livres et modifie la valeur d'un nom de livre dans le code, l'interface utilisateur n'est pas mise à jour.

Si j'implémente INotifyPropertyChanged sur le VM pour Store et modifie la valeur d'un nom de livre dans le code, l'interface utilisateur n'est pas mise à jour.

Si j'implémente INotifyProperyChanged sur le modèle et modifie la valeur d'un nom de livre dans le code, l'interface utilisateur est mise à jour.

L'événement Changed n'est pas déclenché dans le premier cas car le setter Dictionary n'est pas appelé, c'est Item (un livre) l'est.

Suis-je en train de manquer quelque chose parce que si c'est la bonne interprétation, si je veux des notifications cohérentes pour mes modèles, qu'ils soient liés directement à XAML ou via une sorte de collection, je voudrais toujours que le modèle implémente INotifyProperyChanged.

Btw, outre la référence dll, personnellement, je ne vois pas INotifyPropertyChanged comme une fonction d'interface utilisateur - je pense qu'elle devrait être définie dans un espace de noms .net plus général - mes 2 cents.

EDIT COMMENCE ICI:

Nous avons eu un si bon débat sémantique que j'ai manqué le cœur de ma question, donc ici, il est à nouveau affiché, mais avec un exemple MVVM très simple, illustrez ma question.

Les modèles:

public class Book
{
    public string Title { get; set; )
    public List<Author> Authors { get; set; }
}

public class Author
{
    public string Name { get; set; }
}

Le fournisseur de données pour générer des données factices

public class BookProvider
{
    public ObservableCollection<Book> GetBooks() {
        ObservableCollection<Book> books = new ObservableCollection<Book>();

        books.Add(new Book {
            Title = "Book1",
            Authors = new List<Author> { new Author { Name = "Joe" }, new Author { Name = "Phil" } }
        });

        books.Add(new Book {
            Title = "Book2",
            Authors = new List<Author> { new Author { Name = "Jane" }, new Author { Name = "Bob" } }
        });

        return books;
    }
}

Le ViewModel

    public class BookViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Book> books;
    public ObservableCollection<Book> Books {
        get { return books; }
        set {
            if (value != books) {
                books = value;
                NotifyPropertyChanged("Books");
            }
        }
    }

    private BookProvider provider;

    public BookViewModel() {
        provider = new BookProvider();
        Books = provider.GetBooks();
    }

    // For testing the example
    public void MakeChange() {
        Books[0].Title = "Changed";
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(String info) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

Code XAML derrière Ne le ferait pas normalement de cette façon - juste pour un exemple simple

public partial class MainWindow : Window
{
    private BookViewModel vm;

    public MainWindow() {
        InitializeComponent();

        vm = new BookViewModel();
        this.DataContext = vm;
    }

    private void Button_Click(object sender, RoutedEventArgs e) {
        vm.MakeChange();
    }
}

Le XAML

<Window x:Class="BookTest.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="242*" />
        <RowDefinition Height="69*" />
    </Grid.RowDefinitions>
    <ListBox ItemsSource="{Binding Books}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding Title}" />
                    <ListBox ItemsSource="{Binding Authors}">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Name}" FontStyle="Italic" />
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Button Grid.Row="1" Content="Change" Click="Button_Click" />
</Grid>

Comme codé ci-dessus, lorsque je clique sur le bouton et modifie la valeur dans le premier livre, l'interface utilisateur ne change pas.

Cependant, lorsque je déplace INotifyPropertyChanged vers le modèle, cela fonctionne correctement (mises à jour de l'interface utilisateur) car la modification se trouve dans le configurateur de propriétés du modèle et non dans Books dans la machine virtuelle:

public class Book : INotifyPropertyChanged
{
    private string title;
    public string Title {
        get { return title; }
        set {
            if (value != title) {
                title = value;
                NotifyPropertyChanged("Title");
            }
        }
    }

    public List<Author> Authors { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(String info) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

Revenons donc à ma question d'origine, comment puis-je accomplir cela sans implémenter INotifyPropertyChanged dans le modèle?

Merci.

27
IUnknown

Le fait est que si vous suiviez MVVM, vous auriez un BookViewModel pour votre classe de modèle Book. Vous auriez donc une implémentation INotifyPropertyChanged sur ce modèle de vue. Exactement à cette fin, MVVM existe (mais pas seulement).

Cela étant dit, le INotifyPropertyChanged doit être implémenté sur les classes de modèles de vue, pas sur les modèles.

MISE À JOUR : En réponse à votre mise à jour et à notre discussion dans les commentaires ...

Par BookViewModel je voulais dire autre chose. Vous devez encapsuler dans ce modèle de vue non pas l'ensemble de la collection d'objets Book mais un Book individuel:

public class BookViewModel : INotifyPropertyChanged
{
    private Book book;

    public Book Book {
        get { return book; }    
    }

    public string Title {
        get { return Book.Title; }
        set {
            Book.Title = value;
            NotifyPropertyChanged("Title");
        }         
    }

    public BookViewModel(Book book) {
        this.book = book;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(String info) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

Et votre BookProvider renverra ObservableCollection<BookViewModel> au lieu de ObservableCollection<Book>:

public class BookProvider
{
    public ObservableCollection<BookViewModel> GetBooks() {
        ObservableCollection<BookViewModel> books = new ObservableCollection<BookViewModel>();

        books.Add(new BookViewModel(new Book {
            Title = "Book1",
            Authors = new List<Author> { new Author { Name = "Joe" }, new Author { Name = "Phil" } }
        }));

        books.Add(new BookViewModel(new Book {
            Title = "Book2",
            Authors = new List<Author> { new Author { Name = "Jane" }, new Author { Name = "Bob" } }
        }));

        return books;
    }
}

Comme vous pouvez le voir, lorsque vous mettez à jour la propriété Title de la Book, vous le ferez via la propriété Title du modèle de vue correspondant qui augmentera la PropertyChanged événement, qui déclenchera la mise à jour de l'interface utilisateur.

21
Pavlo Glazkov

Veuillez lire cet article . Il explique comment réduire la duplication de code en implémentant INotifyPropertyChanged dans le modèle.

8
Jayesh Shah

Ne confondez pas INotifyPropertyChanged avec MVVM.

Considérez ce qu'est réellement INotifyPropertyChanged -> C'est un événement qui se déclenche pour dire "Hé regarde, j'ai changé". Si quelqu'un s'en soucie, il peut faire quelque chose, qu'il s'agisse d'un View, ViewModel ou autre.

Commençons donc avec votre livre (modèle). La propriété Title peut déclencher un événement modifié, pourquoi ne le ferait-il pas? Cela a du sens, le Livre traite de ses propres propriétés.

Maintenant pour BookViewModel - super, nous n'avons pas besoin de dupliquer le titre et de grouper notre code! Whoo!

Considérons une vue où nous voulons voir une liste de livres, ou un livre avec une liste d'auteurs. Votre ViewModel peut gérer des propriétés supplémentaires spécifiques à la vue, telles que IsSelected. Ceci est un excellent exemple - pourquoi le Livre s'en soucierait-il s'il était sélectionné ou non? C'est la responsabilité du ViewModel.


Évidemment, ce qui précède dépend de votre architecture, mais personnellement, si je crée une bibliothèque d'objets, je vais implémenter une classe de base avec INotifyPropertyChanged et rendre les propriétés de l'objet responsables du déclenchement de l'événement.

5
pfeds