简体   繁体   中英

WPF ~ Trouble with Binding & INotifyPropertyChanged

WPF n00bie here, trying to get his UI to work properly.

So I made this test example. The textblock bound to HeaderText1 changes correctly at the launch of the app, but the textblock bound to HeaderText2 doesn't update after clicking the button.

What am I doing wrong? Thanks in advance!!

<Window x:Class="DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="142.596" Width="310">
    <StackPanel Margin="15">
        <WrapPanel>
            <TextBlock Text="Window title:  " />
            <TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
            <Button Name="btnUpdateSource" Click="btnUpdateSource_Click" Margin="5,0" Padding="5,0">*</Button>
        </WrapPanel>
        <TextBlock Text="{Binding Path=DataContext.HeaderText}"></TextBlock>
        <TextBlock Text="{Binding Path=DataContext.HeaderText2}"></TextBlock>
    </StackPanel>
</Window>

Main window class:

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


namespace DataBinding
{
    public partial class DataContextSample : Window
    {
        public string HeaderText { set; get; }



        public DataContextSample()
        {
            HeaderText = "YES";


            InitializeComponent();
            this.DataContext = this;

        }

        private void btnUpdateSource_Click(object sender, RoutedEventArgs e)
        {

            BindingExpression binding = txtWindowTitle.GetBindingExpression(TextBox.TextProperty);
            binding.UpdateSource();

            Source source = new Source();
            source.HeaderText2 = "YES2";
        }
    }
}

And the INotifyPropertyChanged class

using System.ComponentModel;

namespace DataBinding
{
    public class Source : INotifyPropertyChanged
    {
        public string HeaderText2 { set; get; }

        public event PropertyChangedEventHandler PropertyChanged;


        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

    }
}

First of all you are doing many things wrong.

You should not be using the window as it's own datacontext, you should have a viewmodel that you set.

You should not be using event handlers in the view to manipulate the viewmodel. You should bind the button to a command.

Your source seems to be a "viewmodel", consider renaming it to MainWindowViewModel (for clarity) and then do this.

public class MainWindowViewModel : INotifyPropertyChanged
{
    private string headerText;
    private string headerText2;
    private ICommand updateHeaderText2;

    public string HeaderText
    {
        set
        {
            return this.headerText;
        }
        get 
        {
            this.headerText = value;

            // Actually raise the event when property changes
            this.OnPropertyChanged("HeaderText"); 
        }
    }

    public string HeaderText2
    {
        set
        {
            return this.headerText2;
        }
        get 
        {
            this.headerText2 = value;

            // Actually raise the event when property changes
            this.OnPropertyChanged("HeaderText2");
        }
    }

    public ICommand UpdateHeaderText2
    {
        get 
        {
            // Google some implementation for ICommand and add the MyCommand class to your solution.
            return new MyCommand (() => this.HeaderText2 = "YES2");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

And set this viewmodel to the datacontext of your window.

this.DataContext = new MainWindowViewModel();

And then in your xaml you should bind to the viewmodel as such

<Window x:Class="DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="142.596" Width="310">
    <StackPanel Margin="15">
        <WrapPanel>
            <TextBlock Text="Window title:  " />
            <!-- Not sure what this binding is? -->
            <TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
            <Button Name="btnUpdateSource" Command="{Binding UpdateHeaderText2}" Margin="5,0" Padding="5,0">*</Button>
        </WrapPanel>
        <TextBlock Text="{Binding HeaderText}"></TextBlock>
        <TextBlock Text="{Binding HeaderText2}"></TextBlock>
    </StackPanel>
</Window>

You set the DataContext to this (the window). You don't have a property named HeaderText2 in the DataContext so the second binding won't work.

I'd do this (without changing your code too much, in reality I'd do a proper MVVM approach):

public partial class DataContextSample : Window
{
    public Source Source { get; set; }
    public string HeaderText { set; get; }

    public MainWindow()
    {
        InitializeComponent();

        HeaderText = "YES";
        Source = new Source { HeaderText2 = "YES" };
        DataContext = this;
    }

    private void btnUpdateSource_Click(object sender, RoutedEventArgs e)
    {
        BindingExpression binding = txtWindowTitle.GetBindingExpression(TextBox.TextProperty);
        if (binding != null)
        {
            binding.UpdateSource();
        }

        Source.HeaderText2 = "YES2";
    }
}

I added a new property called Source which is of type Source . Set its initial HeaderText2 to the same "YES" in the constructor and in the button click change that to "YES2" .

You have to change your Source class as well, to actually notify about changes:

public class Source : INotifyPropertyChanged
{
    private string _headerText2;

    public string HeaderText2
    {
        get { return _headerText2; }
        set
        {
            _headerText2 = value;
            OnPropertyChanged("HeaderText2");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

And then in your XAML:

<StackPanel Margin="15">
    <WrapPanel>
        <TextBlock Text="Window title:  " />
        <TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
        <Button Name="btnUpdateSource" Click="btnUpdateSource_Click" Margin="5,0" Padding="5,0">*</Button>
    </WrapPanel>
    <TextBlock Text="{Binding Path=HeaderText}"></TextBlock>
    <TextBlock Text="{Binding Path=Source.HeaderText2}"></TextBlock>
</StackPanel>

Well there are a few issues with your code. First of all, you never assign your "Source" to a datacontext, so there's no way for your second TextBlock to find the value of "HeaderText2".

If however you would assign your "Source" to the textblocks datacontext then we could fetch the value of "HeaderText2". Consider the code below

<Window x:Class="DataBinding.DataContextSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="DataContextSample" Height="142.596" Width="310">
    <StackPanel Margin="15">
        <WrapPanel>
            <TextBlock Text="Window title:  " />
            <TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
            <Button Name="btnUpdateSource" Click="btnUpdateSource_Click" Margin="5,0" Padding="5,0">*</Button>
        </WrapPanel>
        <TextBlock Text="{Binding Path=HeaderText}"></TextBlock>
        <TextBlock Name="TextBlock2" Text="{Binding Path=HeaderText2}"></TextBlock>
    </StackPanel>
</Window>

We have given your second Textblock a name, "TextBlock2" and also removed the "Datacontext"-part from your binding.

Then we have moved the Creation of your "Source" object from the button event to the windows constructor (there is no need to make a new one everytime we click a button when all we want to do is to update a property)

public partial class DataContextSample : Window
{
    public string HeaderText { set; get; }
    private Source source { get; set; }

    public DataContextSample()
    {
        ...

        source = new Source();
        TextBlock2.DataContext = source;

        ...
    }

    ...
}

And then in your buttons click-event we assign your databound property a value of "YES2".

private void btnUpdateSource_Click(object sender, RoutedEventArgs e)
{
    ...

    source.HeaderText2 = "YES2";
}

There is however one more detail. Your class "Source" does implement "INotifyPropertyChanged", but it never "uses" it. By that I mean, that when you assign a value to your property "HeaderText2" you never actually "notify" the UI that something has changed with it, and thus the UI will not fetch the new value. Consider the code below:

public class Source : INotifyPropertyChanged
{        
    public string HeaderText2 { set
        {
            headerText2 = value;
            OnPropertyChanged("HeaderText2");
        }
        get
        {
            return headerText2;
        }
    }
    string headerText2;

    ...
}

So let's take a look at what we've done with the property "HeaderText2". Everytime the "HeaderText2" gets a value assigned, it will first save the value in a privat property (so that we can read from it later). But in addition to that we also call the "OnPropertyChanged" method with our Propertys name. That method will in turn check if anyone is "listening" to our "PropertyChanged"-event (and since we have a databinding on the current object, someone is listening), and create a new event.

Now we have assigned a datasource to your textblock with a path to "HeaderText2", we are notifying all listeners when we update "HeaderText2" on the datasource and we are updating "HeaderText2" on the buttons click event.

Happy coding!

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