简体   繁体   中英

INotifyPropertyChanged Event handler is always null

I use .NETFramework,Version=v4.6.1

I have a Window, MainWindow . This is the XAML:

<Window x:Class="VexLibrary.DesktopClient.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"


        xmlns:local="clr-namespace:VexLibrary.DesktopClient.Views"

        Title="MainWindow" Height="600" Width="800">
    <Grid>
        <StackPanel>
            <Grid Style="{StaticResource TitleBar}">
                <Border Style="{StaticResource TitleBarBorder}">
                    <DockPanel>
                        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                            <TextBlock Style="{StaticResource TitleBarIcon}" Text="&#xE10F;" />
                            <Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>
                        </StackPanel>
                        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
                            <Label Style="{StaticResource TitleBarTime}">12:05 AM</Label>
                            <StackPanel Orientation="Horizontal">
                                <Label Style="{StaticResource TitleBarUsername}">Hassan</Label>
                                <Button>
                                    <TextBlock Style="{StaticResource TitleBarIcon}" Text="&#xE7E8;" />
                                </Button>
                            </StackPanel>
                        </StackPanel>
                    </DockPanel>
                </Border>
            </Grid>
            <Frame Width="700" Height="507" Source="Pages/Dashboard.xaml" />
        </StackPanel>
    </Grid>
</Window>

Note the: <Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>

The DataContext is set as follows in the MainWindow.xaml.cs constructor:

this.DataContext = new MainViewModel();

In the <Frame> , a Page Dashboard.xaml is loaded.

The page Dashboard.xaml has the source:

<Page x:Class="VexLibrary.DesktopClient.Views.Pages.Dashboard"
      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:VexLibrary.DesktopClient.Views.Pages"
      mc:Ignorable="d" 
      d:DesignHeight="460" d:DesignWidth="690"
      Title="Page1">

    <Grid Width="690" Height="460" HorizontalAlignment="Center" VerticalAlignment="Center">
        <!-- Members, Users, Books -->
        <!-- Returns, Subscriptions, Statistics -->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>

        <Button Style="{StaticResource MenuButton}" Grid.Column="0" Grid.Row="0">&#xE125;</Button>
        <Button Style="{StaticResource MenuButton}" Grid.Column="0" Grid.Row="1">&#xE845;</Button>
        <Button Style="{StaticResource MenuButton}" Grid.Column="1" Grid.Row="0">&#xE13D;</Button>
        <Button Style="{StaticResource MenuButton}" Grid.Column="1" Grid.Row="1">&#xE821;</Button>
        <Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="0">&#xE8F1;</Button>
        <Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="1" Command="{Binding ViewStatistics}">&#xEA37;</Button>
    </Grid>
</Page>

In the Dashboard.xaml.cs constructor , I have defined the DataContext like this: DataContext = new DashboardViewModel();

The DashboardViewModel.cs source code is like this (omitted namespaces)

namespace VexLibrary.DesktopClient.ViewModels
{
    class DashboardViewModel : ViewModel
    {
        private MainViewModel parentViewModel;

        public DashboardViewModel()
        {
            this.parentViewModel = new MainViewModel();
        }

        public ICommand ViewStatistics
        {
            get
            {
                return new ActionCommand(p => this.parentViewModel.LoadPage("Statistics"));
            }
        }
    }
}

Now, in this code, notice the Button with the Command :

<Button Style="{StaticResource MenuButton}" Grid.Column="2" Grid.Row="1" Command="{Binding ViewStatistics}">&#xEA37;</Button>

It successfully calls the Command and the parent LoadPage method is executed correctly. The parent viewmodel looks like this:

namespace VexLibrary.DesktopClient.ViewModels
{
    public class MainViewModel : ViewModel
    {
        private string currentPageTitle;

        public string CurrentPageTitle
        {
            get
            {
                return this.currentPageTitle;
            }
            set
            {
                currentPageTitle = value;
                NotifyPropertyChanged();
            }
        }

        public void LoadPage(string pageName)
        {
            this.CurrentPageTitle = pageName;
            Console.WriteLine(CurrentPageTitle);
        }
    }
}

The CurrentPageTitle is successfully updated. However, it is not updated in the view.

The parent view model inherits ViewModel which basically has this code:

namespace VexLibrary.Windows
{
    public abstract class ViewModel : ObservableObject, IDataErrorInfo
    {
        public string this[string columnName]
        {
            get
            {
                return OnValidate(columnName);
            }
        }

        [Obsolete]
        public string Error
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        protected virtual string OnValidate(string propertyName)
        {
            var context = new ValidationContext(this)
            {
                MemberName = propertyName
            };

            var results = new Collection<ValidationResult>();
            bool isValid = Validator.TryValidateObject(this, context, results, true);

            if (!isValid)
            {

                ValidationResult result = results.SingleOrDefault(p =>
                                                                  p.MemberNames.Any(memberName =>
                                                                                    memberName == propertyName));

                return result == null ? null : result.ErrorMessage;
            }

            return null;
        }
    }
}

ObservableObject.cs:

namespace VexLibrary.Windows
{
    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // [CallerMemberName] automatically resolves the property name for us.
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            Console.WriteLine(handler == null);
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

After debugging, I found out, the NotifyPropertyChanged is invoked, but the handler is always null. How do I fix this? This is not updating the text in the MainWindow.xaml. I tested to see if the property value is changed, and yes, it is changed in the MainViewModel.cs

Also, I tested whether the label itself is visible or not. For that, I gave the variable a value and it correctly displays, but it is not updated.

The DashboardViewModel is instantiating a new instance of the MainViewModel rather than using the instance assigned to the DataContext of the MainWindow (and therefore the instance the view is bound to).

For your code to work you need to pass the correct instance of the MainViewModel to the DashboardViewModel as it is this instance that will have a handler for the property changed event.

EDIT: As per the comment below, you should instantiate your sub viewmodels as follows:

namespace VexLibrary.DesktopClient.ViewModels
{
    public class MainViewModel : ViewModel
    {
        private ViewModel _currentViewModel;

        public MainViewModel()
        {
            _currentViewModel = new DashboardViewModel(this);
        }

        public ViewModel CurrentViewModel
        {
            get { return _currentViewModel; }
            private set
            {
                _currentViewModel = value;
                OnPropertyChanged();
            }
        }
    }
}

You can then amend your Xaml such that the frame gets it's data context from the CurrentViewModel property as follows:

<Window x:Class="VexLibrary.DesktopClient.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:VexLibrary.DesktopClient.Views"
        Title="MainWindow" Height="600" Width="800">
    <Grid>
        <StackPanel>
            <Grid Style="{StaticResource TitleBar}">
                <Border Style="{StaticResource TitleBarBorder}">
                    <DockPanel>
                        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                            <TextBlock Style="{StaticResource TitleBarIcon}" Text="&#xE10F;" />
                            <Label Style="{StaticResource TitleBarTitle}" Content="{Binding Path=CurrentPageTitle, UpdateSourceTrigger=PropertyChanged}" ></Label>
                        </StackPanel>
                        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
                            <Label Style="{StaticResource TitleBarTime}">12:05 AM</Label>
                            <StackPanel Orientation="Horizontal">
                                <Label Style="{StaticResource TitleBarUsername}">Hassan</Label>
                                <Button>
                                    <TextBlock Style="{StaticResource TitleBarIcon}" Text="&#xE7E8;" />
                                </Button>
                            </StackPanel>
                        </StackPanel>
                    </DockPanel>
                </Border>
            </Grid>
            <Frame Width="700" Height="507" Source="Pages/Dashboard.xaml" DataContext="{Binding CurrentViewModel}"/>
        </StackPanel>
    </Grid>
</Window>

And will then need to use some form of view location / navigation to change the frame to display the correct view. Some MVVM frameworks (for example, CaliburnMicro) can do this for you.

Again, in order to make this code testable, the instantiation of sub-viewmodels should be delegated to a factory class which is injected into the MainViewModel.

Hope it helps.

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