昨天一個朋友向我求助一個自定義水印控件綁定的問題,問題出在文本框中輸入的文本,不能綁定到
相應的依賴屬性上(自定義的依賴屬性 PassText),他糾結了很久找不出問題所在。問題幫他解決后,這里稍
做總結。
問題描述:
1)默認顯示效果:
2)在“水印密碼框”中輸入四個 ‘a’:
3)單擊按鈕,打印出密碼框中的字符串,但是密碼框中的文字並沒有顯示,顯示的還是定義依賴屬性時
的默認值:
他的部分源代碼:
1)密碼框控件類繼承自 TextBox ,其中依賴屬性 PassText 為綁定到密碼框的屬性:

public class WaterPasswordBox : TextBox { public static readonly DependencyProperty PassTextProperty = DependencyProperty.Register( "PassText", typeof( string ), typeof( WaterPasswordBox ), new PropertyMetadata( "空" ) ); public string PassText { get { return (string)GetValue( PassTextProperty ); } set { SetValue( PassTextProperty, value ); } } public static DependencyProperty WaterContentProprty = DependencyProperty.Register("WaterContent", typeof(object), typeof(WaterPasswordBox), new PropertyMetadata("水印密碼框")); public object WaterContent { get { return GetValue(WaterContentProprty); } set { SetValue(WaterContentProprty, value); } } public static DependencyProperty WaterForegroundProprty = DependencyProperty.Register("WaterForeground", typeof(Brush), typeof(WaterPasswordBox), new PropertyMetadata(new SolidColorBrush(Colors.Gray))); public Brush WaterForeground { get { return (Brush)GetValue(WaterForegroundProprty); } set { SetValue(WaterForegroundProprty, value); } } public WaterPasswordBox() { DefaultStyleKey = typeof(WaterPasswordBox); } ContentControl WaterContentElement = null; PasswordBox PasswordBoxElement = null; public override void OnApplyTemplate() { base.OnApplyTemplate(); WaterContentElement = this.GetTemplateChild("WaterCoElement") as ContentControl; PasswordBoxElement = this.GetTemplateChild("ContentElement") as PasswordBox; if (WaterContentElement != null && PasswordBoxElement != null) { if (string.IsNullOrEmpty(PasswordBoxElement.Password)) WaterContentElement.Visibility = System.Windows.Visibility.Visible; else WaterContentElement.Visibility = System.Windows.Visibility.Collapsed; } } protected override void OnGotFocus(RoutedEventArgs e) { if (WaterContentElement != null && string.IsNullOrEmpty(PasswordBoxElement.Password)) WaterContentElement.Visibility = Visibility.Collapsed; base.OnGotFocus(e); } // public event TextChangedEventHandler TextChanged += ; protected override void OnLostFocus(RoutedEventArgs e) { if (WaterContentElement != null && string.IsNullOrEmpty(PasswordBoxElement.Password)) WaterContentElement.Visibility = Visibility.Visible; base.OnLostFocus(e); } }
2)在 Generic.xaml 文件中定義該控件的樣式:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WaterTextBox" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox"> <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/> </ControlTemplate> <Style TargetType="local:WaterPasswordBox"> <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/> <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/> <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/> <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/> <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/> <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/> <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/> <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/> <Setter Property="Padding" Value="2"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:WaterPasswordBox"> <Grid Background="Transparent"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"/> <VisualState x:Name="Disabled"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Collapsed</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="ReadOnly"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Collapsed</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder"> <DiscreteObjectKeyFrame KeyTime="0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="FocusStates"> <VisualState x:Name="Focused"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Unfocused"/> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}"> <Grid> <ContentControl x:Name="WaterCoElement" Content="{TemplateBinding WaterContent}" FontStyle="Normal" Foreground="{TemplateBinding WaterForeground}" Margin="{StaticResource PhoneTextBoxInnerMargin}" d:LayoutOverrides="Height" Padding="{TemplateBinding Padding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/> <!--重點這里的 Password="{Binding PassText}"綁定不了--> <PasswordBox x:Name="ContentElement" Password="{Binding PassText}" Background="{Binding Background}" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="-12" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/> </Grid> </Border> <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed"> <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
3)在引用該控件的頁面中,添加該控件:
<Border BorderBrush="Blue" BorderThickness="5" Margin="0,10"> <StackPanel> <TextBlock FontSize="30" Text="水印密碼框" Margin="12,0"/> <my:WaterPasswordBox Name="WaterPasswordBox" /> </StackPanel> </Border> <Button BorderBrush="Blue" Tap="UIElement_OnTap" BorderThickness="5" Margin="0,10" Height="100"> 點擊查看斷點測試 </Button>
4)按鈕的 Tap 路由事件:
private void UIElement_OnTap(object sender, GestureEventArgs e) { string temp = WaterPasswordBox.PassText.ToString(); MessageBox.Show(temp); }
之所以在密碼框中輸入的文本沒有最終傳遞到 PassText 這個依賴屬性上,是因為 TemplateBinding
為 Binding 的單向綁定形式,也就是 依賴屬性 PassText 的默認值 “空” 可以綁定到 樣式控件里面的
PasswordBox 控件的 Password 屬性上,但是 不能反向綁定。
截圖:
其中 msdn 對 TemplateBinding 的描述:
“您在模板中使用 TemplateBinding 綁定到模板所應用到的控件的值。
TemplateBinding 比 Binding 有效,但較少功能。 使用 TemplateBinding 使用與 RelativeSource 屬性的 Binding 等效設置為
RelativeSource.TemplatedParent。 ”
“TemplateBinding是Binding的一個輕量級版本,它失去了成熟版本Binding的很多功能,比如繼承內容引用(inheritence context referencing),
RelativeSource引用,還有通過IValueConverter/TypeConverter機制的動態類型轉換。它僅支持由模板產生的FrameworkElements,它的數據源引
用會指向模板中的父級元素。TemplateBinding最主要的用途是內置在模板中綁定模板化元素的屬性,在這種情況下,比起成熟Binding效率要高得多。”
也就是:
<PasswordBox Password="{TemplateBinding PassText}"/>
等價於:
<PasswordBox Password="{Binding Path=PassText, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"/>
這時,把自定義密碼框樣式中的 PasswordBox 控件的綁定中,把 Binding 的設置 Mode=OneWay 改成 Mode=TwoWay,就可以實現雙向綁定了,
截圖:
此時雖然完成了雙向綁定,但是遇到另一個問題,如果在自定義密碼框中輸入文本后,立即點擊按鈕時,彈出框
中顯示的還是自定義密碼框中之前綁定屬性值;如果在填寫完成自定義密碼框后,單擊屏幕其它地方,讓密碼框
失去焦點,然后再單擊按鈕,就能顯示正確的內容了。
造成這個問題的原因,是因為 這里注冊的是按鈕的 Tap 這個路由事件,而它的執行時間要早於文本框 LostFocus
事件。這里再為按鈕添加一個 Click 事件,為 自定義密碼框控件添加一個 LostFocus 事件,同是在 C# 頁面打印出
按鈕的執行信息:
XAML:
<my:WaterPasswordBox Name="WaterPasswordBox" LostFocus="WaterPasswordBox_LostFocus"/> <Button BorderBrush="Blue" Tap="UIElement_OnTap" BorderThickness="5" Margin="0,10" Height="100" Click="Button_Click"> 點擊查看斷點測試 </Button>
C# :
private void UIElement_OnTap(object sender, GestureEventArgs e) { Debug.WriteLine("UIElement_OnTap"); //WaterPasswordBox.PassText = "asd"; //string temp = WaterPasswordBox.PassText.ToString(); //MessageBox.Show(temp); } private void Button_Click(object sender, RoutedEventArgs e) { Debug.WriteLine("Button_Click"); string temp = WaterPasswordBox.PassText.ToString(); MessageBox.Show(temp); } private void WaterPasswordBox_LostFocus(object sender, RoutedEventArgs e) { Debug.WriteLine("WaterPasswordBox_LostFocus"); }
在 Debug 輸出窗口中輸出:
UIElement_OnTap
WaterPasswordBox_LostFocus
Button_Click
可以看出,Tap 路由事件的觸發要早於 LostFocus 事件,最后觸發 Click 事件。
此時再在 文本框中輸入4個 ‘a’ ,從按鈕的 Click 事件中把密碼框中的文本打印出來:
雖然實現了雙向綁定,但是從 PasswordBox 的 Password 屬性值反向同步並不是及時的,參考了一下
下面的其它屬性,暫時還沒有找到在 TextChanged 事件里就能觸發的 update source 的設置:
由於時間的關系,有時間再去查相應文檔。如果有朋友不幸閱讀了本文,並且知道原因,希望能指點一下。