WPF MVVM INotifyPropertyChanged 实现 - 模型或 ViewModel

发布于 2024-10-21 07:59:35 字数 5813 浏览 7 评论 0原文

我在 StackOverflow 和其他博客上阅读了许多关于在哪里实现 INotifyPropertyChanged 的​​争论,但似乎在某些情况下您必须在模型上实现它。这是我的情况 - 我正在寻找有关我的结论的反馈,或者我的方法是否错误。

我正在使用 ObservableDictionary 的实现(ObservableDictionary),因为我需要使用该键进行高性能查询。

在这本字典中,我放置了模型对象的集合。

在我的 VM 中,我声明了字典的一个实例(Books),并在 XAML 中绑定到它。

    <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>  

如果我在图书虚拟机上实现 INotifyPropertyChanged 并在代码中更改图书名称的值,则 UI 不会更新。

如果我在 Store 的 VM 上实现 INotifyPropertyChanged 并更改代码中书籍名称的值,则 UI 不会更新。

如果我在模型上实现 INotifyProperyChanged 并更改代码中书籍名称的值,则 UI 会更新。

第一种情况不会触发 Changed 事件,因为没有调用 Dictionary setter,而是调用 Item(一本书)。

我是否遗漏了一些东西,因为如果这是正确的解释,如果我想要模型的一致通知,无论它们是直接来自 XAML 还是通过某种集合绑定,我总是希望模型实现 INotifyProperyChanged。

顺便说一句,除了 dll 引用之外,我个人并不认为 INotifyPropertyChanged 作为 UI 函数 - 认为它应该定义在更通用的 .net 命名空间中 - 我的 2 美分。

编辑从这里开始:

我们进行了一场很好的语义辩论,以至于我错过了问题的核心,所以在这里再次发布它,但用一个非常简单的 MVVM 示例说明了我的问题。

模型:

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

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

生成一些虚拟数据的数据提供程序

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;
    }
}

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));
        }
    }
}

背后的 XAML 代码 通常不会这样 - 只是为了简单的例子

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();
    }
}

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>

如上面的代码所示,当我单击按钮并更改第一本书中的值时,用户界面不会更改。

但是,当我将 INotifyPropertyChanged 移至模型时,它工作正常(UI 更新),因为更改是在模型属性设置器中而不是虚拟机中的 Books 中:

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));
        }
    }
}

所以回到我原来的问题,如何在模型中实现 INotifyPropertyChanged 的​​情况下完成此操作?

谢谢。

I have read a number of debates on where to implement INotifyPropertyChanged here on StackOverflow and other blogs but it seems that there are cases where you have to implement it on the Model. Here is my scenario - I am looking for feedback on my conclusion or is my approach wrong.

I am using this implementation of an ObservableDictionary (ObservableDictionary) because I need performant queries using the key.

In this dictionary I place the collection of Model objects.

In my VM, I declare an instance (Books) of the dictionary and in the XAML bind to it.

    <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>  

If I implement INotifyPropertyChanged on the VM for Books and change the value of a Book name in code, the UI is not updated.

If I implement INotifyPropertyChanged on the VM for Store and change the value of a Book name in code, the UI is not updated.

If I implement INotifyProperyChanged on the Model and change the value a Book name in code, the UI is updated.

The Changed event is not fired in the first case because the Dictionary setter is not called, it's Item (a Book) is.

Am I missing something because if this is the correct interpretation, if I want consistent notifications for my Models regardless of whether they are bound to directly from XAML or via some sort of collection, I would always want the Model to implement INotifyProperyChanged.

Btw, besides the dll reference, I personally do no see INotifyPropertyChanged as a UI function - think it should be defined in a more general .net namespace - my 2 cents.

EDIT STARTS HERE:

We were having such a good semantics debate that I missed the core of my question so here it is posted again but with a very simple MVVM example illustrate my question.

The Models:

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

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

The Data Provider to generate some dummy data

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;
    }
}

The 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));
        }
    }
}

XAML code behind
Would not normally to it this way - just for simple example

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();
    }
}

The 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>

As coded above, when I click on the button and change the value in the first Book, the UI does not change.

However, when I move the INotifyPropertyChanged to the Model it works fine (UI updates) because the change is in the Model property setter not Books in the VM:

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));
        }
    }
}

So back to my original question, how do I accomplish this without implementing INotifyPropertyChanged in the Model?

Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

舞袖。长 2024-10-28 07:59:35

问题是,如果您遵循 MVVM,您的 Book 模型类就会有一个 BookViewModel。因此,您将在该视图模型上实现 INotifyPropertyChanged 。正是出于这个目的,MVVM 才存在(但不仅如此)。

话虽这么说,INotifyPropertyChanged 必须在视图模型类而不是模型上实现。

更新:为了回应您的更新和我们在评论中的讨论...

通过 BookViewModel 我的意思是别的东西。您需要在此视图模型中包装的不是整个 Book 对象集合,而是单个 Book

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));
        }
    }
}

并且您的 BookProvider 将返回 ObservableCollection< ;BookViewModel> 而不是 ObservableCollection

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;
    }
}

如您所见,当您更新 BookTitle 属性时您将通过相应视图模型的 Title 属性来执行此操作,该属性将引发 PropertyChanged 事件,从而触发 UI 更新。

The thing is that if you were following MVVM, you would have a BookViewModel for your Book model class. So you would have a INotifyPropertyChanged implementation on that view model. Exactly for that purpose MVVM exists (but not only).

That being said, the INotifyPropertyChanged has to be implemented on view model classes, not models.

UPDATE: In response to your update and our discussion in comments...

By BookViewModel I meant something else. You need to wrap in this view model not the whole collection of Book objects but an individual Book:

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));
        }
    }
}

And your BookProvider will return ObservableCollection<BookViewModel> instead of 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;
    }
}

As you can see, when you are updating the Title property of the Book you will be doing it through the Title property of the corresponding view model that will raise the PropertyChanged event, which will trigger the UI update.

青春有你 2024-10-28 07:59:35

请阅读这篇文章。它解释了如何通过在模型中实现 INotifyPropertyChanged 来减少代码重复。

Please read this article. It explains how you can reduce code duplication by implementing INotifyPropertyChanged in the model.

粉红×色少女 2024-10-28 07:59:35

不要将 INotifyPropertyChanged 与 MVVM 混淆。

考虑一下 INotifyPropertyChanged 实际上是什么 ->这是一个让人兴奋地说“嘿,看,我变了”的事件。如果有人关心,那么他们可以对此做一些事情,无论他们是 View、ViewModel 还是其他什么。

那么让我们从您的书(模型)开始。 Title 属性可以触发更改事件,为什么不呢?这是有道理的,这本书正在处理它自己的属性。

现在对于 BookViewModel - 太棒了,我们不需要重复标题并增加我们的代码!呼!

考虑一个视图,我们希望在其中查看书籍列表或包含作者列表的书籍。您的 ViewModel 可以处理特定于视图的其他属性,例如 IsSelected。这是一个很好的例子——为什么本书会关心它是否被选中?这是 ViewModel 的职责。


显然,上述内容取决于您的体系结构,但就我个人而言,如果我要创建一个对象库,我将使用 INotifyPropertyChanged 实现一个基类,并使对象属性负责触发事件。

Don't confuse INotifyPropertyChanged with MVVM.

Consider what INotifyPropertyChanged actually is -> It's an event that fires to say "Hey look, I've changed". If anyone cares then they can do something about it, whether they're a View, ViewModel, or whatever.

So let's start with your Book (Model). The Title property can fire a changed event, why would it not? It makes sense, the Book is dealing with it's own properties.

Now for BookViewModel - great, we don't need to duplicate the Title and bulk up our code! Whoo!

Consider a View where we want to see a list of books, or a book with a list of authors. Your ViewModel can handle additional properties specific for the view, such as IsSelected. This is a great example - why would the Book care if it was selected or not? This is the responsibility of the ViewModel.


Obviously the above depends on your architecture, but personally if I'm creating an object library I'll implement a base class with INotifyPropertyChanged and make the object properties responsible for firing the event.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文