简体   繁体   中英

Updating a Bound Property does not update the View in WPF

I am playing around with the MVVM pattern.
I have built a simple application that includes just
-a ListView that displays a simple collection of a UserControl,
-a Button that adds new items to the ListView and
-a ContextMenu that should allow the user to edit the SelectedItem.


The problem:

I have two ICommands bound to my View. ItemBeginEdit and ItemEndEdit .
They are both called properly throughout the app and the parameters are passed through correctly. Tested.
The CommandParameter for each Command is the Bound source of the ListView.SelectedItem (UserControl).
To make it clear:
- ListView.ItemSource = Collection of Models.Item
- ListView.DataTemplate = UserControls.Item
- UserControls.Item.Source = DependencyProperty of type Models.Item .
When changing the whole Collection , the ListView displays correctly all of my items.
When I am getting a reference of Models.Item from the ListView.SelectedItem and change one of its properties in the ICommand execution, it is not updated on the UI.
I have noticed that if I edit the Collection in the Constructor of my ViewModel , the UI updates just for that time only.
If I try to changed it back through the ICommand , nothing happens.
INotifyPropertyChanged is implemented to the relevant classes.
I am still learning WPF and XAML. What do I miss?


The code:

- MainWindow.xaml (View)

<Window x:Class="Demo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Demo"
    xmlns:uc="clr-namespace:Demo.Views.UserControls"
    xmlns:vm="clr-namespace:Demo.ViewModels"
    xmlns:b="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    mc:Ignorable="d"
    x:Name="windowMain"
    Title="Demo"
    Height="300"
    Width="300"
    WindowStartupLocation="CenterScreen">

<Window.DataContext>
    <vm:MainVM />
</Window.DataContext>

<StackPanel>
    <Button x:Name="btnAddItem"
            Content="Add"
            Width="150"
            Command="{Binding ItemNew}" />       
    <ListView x:Name="lviewCollection"
              Width="150"
              MinHeight="100"
              HorizontalContentAlignment="Stretch"
              ItemsSource="{Binding Items}"
              SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <b:Interaction.Triggers>
            <b:EventTrigger EventName="LostFocus">
                <b:InvokeCommandAction Command="{Binding ItemEndEdit}"
                                       CommandParameter="{Binding SelectedItem}" />
            </b:EventTrigger>
        </b:Interaction.Triggers>
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                    <uc:Item DataSource="{Binding}" 
                             IsEditing="{Binding IsEditing}">
                    </uc:Item>

                    <StackPanel.ContextMenu>
                        <ContextMenu DataContext="{Binding Source={x:Reference Name=windowMain}, Path=DataContext}">
                            <MenuItem Header="Rename"
                                      Command="{Binding ItemBeginEdit}"
                                      CommandParameter="{Binding SelectedItem}" />
                        </ContextMenu>
                    </StackPanel.ContextMenu>
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</StackPanel>


- MainVM.cs (ViewModel)

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Demo.ViewModels
{
    public class MainVM : INotifyPropertyChanged
    {
        //INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string PropertyName = "")
        {
            var args = new PropertyChangedEventArgs(PropertyName);
            PropertyChanged?.Invoke(this, args);
        }

        //Properties 
        public ObservableCollection<Models.CustomItem> Items { get; set; }

        private Models.CustomItem selectedItem;
        public Models.CustomItem SelectedItem
        {
            get { return selectedItem; }
            set
            {
                if (value == null)
                    return;
                if (value == selectedItem)
                    return;

                selectedItem = value;
                OnPropertyChanged();
            }
        }

        public Commands.CmdItemNew ItemNew { get; set; }
        public Commands.CmdItemBeginEdit ItemBeginEdit { get; set; }
        public Commands.CmdItemEndEdit ItemEndEdit { get; set; }

        //Constructors 
        public MainVM()
        {
            Items = new ObservableCollection<Models.CustomItem>();
            SelectedItem = null;
            ItemNew = new Commands.CmdItemNew(this);
            ItemBeginEdit = new Commands.CmdItemBeginEdit(this);
            ItemEndEdit = new Commands.CmdItemEndEdit(this);

            ReadItems();
        }

        //Methods
        public void SetupItems()
        {
            //Just create demo items. Not used. 
            var items = DatabaseHelper.ReadItems();
            int itemsToCreate = 3 - items.Length;
            while (itemsToCreate > 0)
            {
                CreateItem();
                itemsToCreate--;
            }
        }

        public void CreateItem()
        {
            var item = new Models.CustomItem()
            {
                Name = "New Item"
            };
            DatabaseHelper.Insert(item);
        }

        public void ReadItems()
        {
            var items = DatabaseHelper.ReadItems();
            Items.Clear();
            foreach (var item in items)
                Items.Add(item);
        }
    }
}

- CustomItem.cs (Model)

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using SQLite;

namespace Demo.Models
{
    public class CustomItem : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                var args = new PropertyChangedEventArgs(PropertyName);
                PropertyChanged.Invoke(this, args);
            }
        }
        #endregion

        //Properties 
        private int id;
        [PrimaryKey, AutoIncrement]
        public int Id
        {
            get { return id; }
            set
            {
                if (value != id)
                {
                    id = value;
                    OnPropertyChanged();
                }
            }
        }

        private string name;
        public string Name
        {
            get { return name; }
            set
            {
                if (value != name)
                {
                    name = value;
                    OnPropertyChanged();
                }
            }
        }

        private bool isEditing;
        [Ignore]
        public bool IsEditing
        {
            get
            {
                return isEditing;
            }
            set
            {
                if (value != isEditing)
                {
                    isEditing = value;
                    OnPropertyChanged();
                }
            }
        }
    }
}

- CmdItemBeginEdit.cs (ICommand)

using System;
using System.Windows.Input;

namespace Demo.ViewModels.Commands
{
    public class CmdItemBeginEdit : ICommand
    {
        #region ICommand implementation 
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public bool CanExecute(object parameter)
        {
            return true;
        }
        public void Execute(object parameter)
        {
            var item = parameter as Models.CustomItem;
            if (item == null)
                return;

            item.IsEditing = true;
        }
        #endregion

        //Properties
        public MainVM VM { get; set; }

        //Constructors 
        public CmdItemBeginEdit(MainVM VM)
        {
            this.VM = VM;
        }
    }
}

- CmdItemEndEdit.cs (ICommand)

using System;
using System.Windows.Input;

namespace Demo.ViewModels.Commands
{
    public class CmdItemEndEdit : ICommand
    {
        #region ICommand implementation 
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public bool CanExecute(object parameter)
        {
            return true;
        }
        public void Execute(object parameter)
        {
            var item = parameter as Models.CustomItem;
            if (item == null)
                return;

            item.IsEditing = false;
        }
        #endregion

        //Properties
        public MainVM VM { get; set; }

        //Constructors 
        public CmdItemEndEdit(MainVM VM)
        {
            this.VM = VM;
        }
    }
}

- Item.xaml (UserControl)

<UserControl x:Class="Demo.Views.UserControls.Item"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:Demo.Views.UserControls"
         mc:Ignorable="d"
         d:DesignHeight="45"
         d:DesignWidth="90">

    <StackPanel HorizontalAlignment="Left"
                VerticalAlignment="Center">
        <TextBlock x:Name="tblockName"
                   Text="Name"
                   Margin="2"
                   FontSize="15"
                   VerticalAlignment="Center" />
        <TextBox x:Name="tboxInput"
                 Text="Name"
                 Visibility="Collapsed"
                 Margin="2"
                 FontSize="14"
                 FontStyle="Italic"
                 VerticalAlignment="Center">
        </TextBox>
    </StackPanel>
</UserControl>

- Item.xaml.cs (UserControl)

using System;
using System.Windows;
using System.Windows.Controls;

namespace Demo.Views.UserControls
{
    public partial class Item : UserControl
    {
        #region DependencyProperty - DataSource
        public Models.CustomItem DataSource
        {
            get { return (Models.CustomItem)GetValue(DataSourceProperty); }
            set { SetValue(DataSourceProperty, value); }
        }
        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(Models.CustomItem), typeof(Item), new PropertyMetadata(null, SetDataSource));
        private static void SetDataSource(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as Item;
            var value = e.NewValue as Models.CustomItem;

            if (value == null)
            {
                control.tblockName.Text = string.Empty;
                control.IsEditing = false;
            }
            else
            {
                control.tblockName.Text = value.Name;
                control.IsEditing = value.IsEditing;
            }
        }
        #endregion


        #region DependencyProperty - IsEditing
        public bool IsEditing
        {
            get { return (bool)GetValue(IsEditingProperty); }
            set { SetValue(IsEditingProperty, value); }
        }
        public static readonly DependencyProperty IsEditingProperty =
            DependencyProperty.Register("IsEditing", typeof(bool), typeof(Item), new PropertyMetadata(false, SetIsEditing));
        private static void SetIsEditing(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as Item;
            var value = (bool)(e.NewValue);

            if (value)
            {
                control.tblockName.Visibility = Visibility.Collapsed;
                control.tboxInput.Visibility = Visibility.Visible;
            }
            else
            {
                control.tboxInput.Visibility = Visibility.Collapsed;
                control.tblockName.Visibility = Visibility.Visible;
            }
        }
        #endregion

        //Constructors 
        public Item()
        {
            InitializeComponent();
        }
    }
}

It seems that you are assuming that the DataSource property of the Item control is set whenever a property of the referenced CustomItem class changes. That is not the case.

You should remove the DataSource property and add properties that are bound to the view model item properties like this:

<ListView.ItemTemplate>
    <DataTemplate>
        <StackPanel>
            <uc:Item Text="{Binding Name}" ... />
            ...
        </StackPanel>
    </DataTemplate>
</ListView.ItemTemplate>

The elements in the UserControl's XAML will bind to its properties like

<TextBlock Text="{Binding Text,
                  RelativeSource={RelativeSource AncestorType=UserControl}}"/>

An alternative to exposing a set of UserControl properties would be that the elements in the UserControl's XAML bind directly to the view model properties, like

<TextBlock Text="{Binding Name}" ... />

Then ItemTemplate would then simply contain this:

<uc:Item />

This would however make the UserControl dependent on the view model type (or more precisely on any type that exposes an equivalent set of properties).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM