WPF用户控件绑定模式为OneWay

6

我正在尝试创建一个带有可绑定属性的WPF用户控件(也许更好地说是"开发人员控件")。我的代码包括以下文件:

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

有一个“MyDataObject”类的实例,它有一个“StringData”属性,该属性定期使用计时器进行修改。我的用户控件绑定到它的属性“StringData”。如果在“MainWindow.xaml”文件中的绑定设置为“TwoWay”,则用户控件将不断更新,但如果我使用“OneWay”绑定,则用户控件仅更新一次,然后“MyDataObject”类的实例的“PropertyChanged”事件不再触发,因为突然间它没有订阅者。
为什么“OneWay”绑定在被调用一次后就停止工作了? 哪些代码更改可以使“TwoWay”和“OneWay”绑定都继续工作?

当您遇到任何Binding问题时,您应查看Visual Studio的输出窗口中显示的错误,它们会告诉您问题所在。 - Sheridan
2个回答

1
首先。
this.MLTBLabel.DataContext = this;
this.MLTBTextBox.DataContext = this;

不要!

永远不要在代码后台设置你的DataContext。一旦这样做,你就失去了从父控件绑定到用户控件的依赖属性的神奇之美。换句话说,不要这样做。

以下是你应该做的:

给你的UserControl一个x:Name

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

将您的用户控件的依赖属性绑定到元素上,像这样:
<TextBlock Text="{Binding MyDependencyProperty, ElementName=usr}" ... />

将您的用户控件的 DataContext 属性绑定到元素上,就像这样:

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

使用这种方法可以让您在MainWindow中设置UserControlDataContext,但仍然能够在UserControl内绑定到其依赖属性。如果您在代码后台设置了UserControl的DataContext,则无法绑定到其依赖属性。
现在,进入您的实际问题。
所有这些:
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
    }

忘了吧。看起来你试图绕开我之前提到的不当行为。你应该绑定到你的依赖属性:
<Label Grid.Row="0" Grid.Column="0" Text="{Binding Text, ElementName=usr}"/>

你还面临一个问题,就是在你的MainWindow中,你对UserControl使用了一个绑定。

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

现在,由于您已经在代码后台中设置了DataContext。这实际上是在说:

从当前控件的DataContext绑定到StringData。

在您的情况下,这是与您的MainWindow DataContext完全不同的绑定。(因为您已经在UserControl中明确设置了DataContext)。

按照我之前提到的运行一遍。学习的内容很多,但这是一个开始。


非常抱歉,我无法理解以“<TextBlock”开头的行。在我的控件“MyLabelledTextBox”中,我没有TextBlocks,只有一个TextBox。我应该像这样将绑定放在TextBox中吗?<TextBox x:Name="MLTBTextBox" ... Text="{Binding Text, ElementName=usr}" /> - Aspro
是的,没错。我提供的代码只是示例。 - Mike Eason
我该如何使用以“<TextBlock”开头的第二行?你举例使用了“TextBlock”、“MyDependencyProperty”和“MyDataContextProperty”,但我引入了一个特定的TextBox元素和一个名为“TextProperty”的特定依赖属性。我想知道如何处理它们,以及在必要时应该引入哪些新的属性。 - Aspro
我的用户控件中只有一个文本框,这是我想要绑定到“Text”依赖属性的唯一控件。然后我还有一个标签,但它可以保持未绑定状态。 - Aspro
设置UserControl的DataContext是您的窗口的责任:<testBinding:MyLabelledTextBox DataContext="..." - Mike Eason
显示剩余3条评论

0

在我看来,应该是这一行:

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

第一次计时器触发时,this.StringData为null,因此表达式中的'??'返回string.Empty。然后检查长度是否大于等于4。它不是,所以将this.StringData从null设置为string.Empty。由于属性仅在更改时更新,因此INotifyPropertyChanged只触发一次。
第二次,我们从string.Empy转到string.Empty,因此INotifyPropertyChanged不会触发,因为没有变化。
基本上,计时器正在触发,但this.StringData现在卡在string.Empty上,这意味着INotifyPropertyChanged忽略它。这是有道理的-如果属性实际上没有更改,为什么WPF运行时要费力地从C#属性向GUI推送更新呢?这只会减慢速度而没有任何收益。
如果使用双向绑定,所有这些都会改变。如果this.StringData的长度设置为4个或更多字符,则会像赛马一样执行代码,每10秒钟追加另一个“*”。
因此,如果在启动时将`this.StringData`设置为“****”,它将与OneWay或TwoWay绑定配合使用,并且您将观察到随着计时器的触发字符串长度增加。
当然,如果设置为OneWay绑定,则字符串将连续添加一个*,并且它不会响应用户输入。我认为OneWay是OneWayFromSource的速记方式,因此C#属性中的更改将被推送到XAML中,但来自XAML的任何更改都不会被推回到C#中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接