简体   繁体   中英

WPF UserControl with Binding Mode=OneWay

I am trying to make a sample WPF user control (maybe it would be better to say “developer control”) with bindable properties. My code consists of these files:

----- MainWindow.xaml -----
<Window x:Class="Test_Binding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:testBinding="clr-namespace:Test_Binding"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <testBinding:MyLabelledTextBox x:Name="MLTB" LabelText="My custom control: MyLabelledTextBox" Text="{Binding StringData, Mode=OneWay}" />
    </StackPanel>
</Window>

----- MainWindow.xaml.cs -----
using System.Windows;

namespace Test_Binding
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.DataContext = new MyDataObject();
            this.InitializeComponent();
        }
    }
}

----- MyDataObject.cs -----
using System.Runtime.CompilerServices; // CallerMemberName
using System.ComponentModel; // INotifyPropertyChanged

namespace Test_Binding
{
    public class MyDataObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string stringData;
        public string StringData
        {
            get { return this.stringData; }
            set
            {
                if (value != this.stringData)
                {
                    this.stringData = value;
                    this.OnPropertyChanged();
                }
            }
        }

        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public MyDataObject()
        {
            System.Timers.Timer t = new System.Timers.Timer();
            t.Interval = 10000;
            t.Elapsed += t_Elapsed;
            t.Start();
        }

        private void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            this.StringData = ((this.StringData ?? string.Empty).Length >= 4 ? string.Empty : this.StringData + "*");
        }

    }
}

----- MyLabelledTextBox.xaml -----
<UserControl x:Class="Test_Binding.MyLabelledTextBox"
             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"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <StackPanel Background="Yellow">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*" />
            <ColumnDefinition Width="0.5*" />
        </Grid.ColumnDefinitions>   
        <Label x:Name="MLTBLabel" Grid.Row="0" Grid.Column="0" />
        <TextBox x:Name="MLTBTextBox" Grid.Row="0" Grid.Column="1" Background="Yellow" Text="{Binding Text, Mode=TwoWay}" />
    </Grid>
  </StackPanel>
</UserControl>

----- MyLabelledTextBox.xaml.cs -----
using System.Windows;
using System.Windows.Controls;

namespace Test_Binding
{
    /// <summary>
    /// Interaction logic for MyLabelledTextBox.xaml
    /// </summary>
    public partial class MyLabelledTextBox : UserControl
    {
        public static readonly DependencyProperty LabelTextProperty =
            DependencyProperty.Register("LabelText", typeof(string), typeof(MyLabelledTextBox),
            new PropertyMetadata(string.Empty, MyLabelledTextBox.LabelTextPropertyChanged));
        public string LabelText
        {
            get { return (string)this.GetValue(MyLabelledTextBox.LabelTextProperty); }
            set { this.SetValue(MyLabelledTextBox.LabelTextProperty, value); }
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(MyLabelledTextBox),
            new PropertyMetadata(string.Empty, MyLabelledTextBox.TextPropertyChanged));
        public string Text
        {
            get { return (string)this.GetValue(MyLabelledTextBox.TextProperty); }
            set { this.SetValue(MyLabelledTextBox.TextProperty, value); }
        }

        public MyLabelledTextBox()
        {
            this.InitializeComponent();

            this.MLTBLabel.DataContext = this;
            this.MLTBTextBox.DataContext = this;
            this.MLTBTextBox.TextChanged += new TextChangedEventHandler(this.MLTBTextBox_TextChanged);
        }

        private void MLTBTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            this.Text = this.MLTBTextBox.Text; // transfer changes from TextBox to bindable property (bindable property change notification will be fired)
        }

        private static void LabelTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((MyLabelledTextBox)d).MLTBLabel.Content = (string)e.NewValue; // transfer changes from bindable property to Label
        }

        private static void TextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((MyLabelledTextBox)d).MLTBTextBox.Text = (string)e.NewValue; // transfer changes from bindable property to TextBox
        }
    }
}

There is an instance of the “MyDataObject” class with the property “StringData”, which is modified periodically using a timer. My user control is bound to its property “StringData”. If the binding in the “MainWindow.xaml” file is set as “TwoWay”, the user control keeps being updated, but if I use the “OneWay” binding, then the user control is updated a single time and then the “PropertyChanged” event of the instance of the “MyDataObject” class does not fire again, because suddenly it has no subscriber.

Why does the “OneWay” binding stop working after being invoked once ? What code change would allow both the “TwoWay” and “OneWay” bindings to keep working ?

Firstly.

this.MLTBLabel.DataContext = this;
this.MLTBTextBox.DataContext = this;

Noooooooooooooooo!

Never. Ever. Ever. Set your DataContext from code-behind. As soon as you do this, you lose the magical beauty of binding to your user control's dependency properties from your parent control. In other words, just don't do it.

Here's what you should do:

Give your UserControl an x:Name .

<UserControl ...
    x:Name="usr">

Bind your UserControl's Dependency Properties to your elements, like this:

<TextBlock Text="{Binding MyDependencyProperty, ElementName=usr}" ... />

Bind your UserControl's DataContext properties to your elements, like this:

<TextBlock Text="{Binding MyDataContextProperty}"/>

Using this method will allow you to set the DataContext of your UserControl in the MainWindow , but still be able to bind to the UserControl's dependency properties within the UserControl. If you set the DataContext of your UserControl in the code-behind, you will not be able to bind to your Dependency Properties.

Now, onto your actual problem.

All of this:

private void MLTBTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        this.Text = this.MLTBTextBox.Text; // transfer changes from TextBox to bindable property (bindable property change notification will be fired)
    }

    private static void LabelTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyLabelledTextBox)d).MLTBLabel.Content = (string)e.NewValue; // transfer changes from bindable property to Label
    }

    private static void TextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyLabelledTextBox)d).MLTBTextBox.Text = (string)e.NewValue; // transfer changes from bindable property to TextBox
    }

Forget about it. Looks like you're trying to get round the wrongdoings that I spoke about earlier.

You should be binding to your dependency properties instead:

<Label Grid.Row="0" Grid.Column="0" Text="{Binding Text, ElementName=usr}"/>

Another issue that you have is that in your MainWindow , you are using a binding on your UserControl .

Text="{Binding StringData, Mode=OneWay}"

Now, as you have already set your DataContext in code-behind. What this is effectively saying is:

Bind to StringData from the DataContext of the current control.

Which in your case, is a totally different binding from your MainWindow DataContext. (As you have explicitly set the DataContext in your UserControl).

Run through what I mentioned earlier. There's a lot to learn, but it's a start.

It looks to me like its this line:

this.StringData = ((this.StringData ?? string.Empty).Length >= 4 ? string.Empty : this.StringData + "*");
    }

The first time the timer fires, this.StringData is null so the '??' in the expression returns string.Empty . It then checks if the length is >= 4. It isnt, so it sets this.StringData from null to string.Empty . As properties only update on a change, then INotifyPropertyChanged fires once.

The second time, we go from string.Empy to string.Empty , so INotifyPropertyChanged does not fire, as there is no change.

Essentially, the timer is firing, but this.StringData is now stuck on string.Empty , which means INotifyPropertyChanged ignores it. This makes sense - why should the WPF runtime go to the trouble of pushing an update from the C# property to the GUI, if the property hasn't actually changed? This would just slow things down for no gain.

This all changes if you use two way binding. If this.StringData ever gets set to a length of 4 characters or more, then its away like a racehorse: it will execute the code to append another "*" to it every 10 seconds.

Thus, if you set this.StringData to "****" on startup, it will work with OneWay or TwoWay binding, and you will observe the string increasing in length as the timer fires.

Of course, if set to OneWay binding, the string will just have a * added to it continuously, and it won't respond to user input. I think of OneWay as shorthand for OneWayFromSource , so the changes in the C# property will be pushed into the XAML, but any changes from the XAML will not get pushed back into the C#.

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