如何实现自定义UserControl依赖属性与父控件DataContext双向绑定?
我有两个WPF UserControl:MainView作为主UI控件,其DataContext绑定到包含SelectedFile属性的ViewModel;自定义控件FileSelectControl声明了SelectedFilepath依赖属性。在MainView的XAML中,将FileSelectControl的SelectedFilepath与ViewModel的SelectedFile进行绑定。
当前绑定仅单向生效:修改MainView中的TextBox内容时,能同步更新FileSelectControl的SelectedFilepath属性;但通过FileSelectControl的RelayCommand选择文件并修改SelectedFilepath属性时,该变更无法同步到ViewModel的SelectedFile属性。需要在保持FileSelectControl封装性的前提下,解决双向同步的问题。
相关代码
MainView.xaml
<UserControl x:Class="Whiteking_UnitTest_Updater.Views.MainView" 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:Whiteking_UnitTest_Updater.Views" xmlns:viewmodel="clr-namespace:Whiteking_UnitTest_Updater.ViewModels" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Background="Black" d:DataContext="{d:DesignInstance Type=viewmodel:MainViewModel}"> <Grid> <Border BorderBrush="Lime" BorderThickness="1"/> <local:FileSelectControl Height="30" Width="500" SelectedFilepath="{Binding SelectedFile, UpdateSourceTrigger=PropertyChanged}"/> <TextBox Text="{Binding SelectedFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,100,0,0"/> <Button x:Name="CloseButton" Content="EXIT" Margin="50,35" Padding="15,5" FontSize="18" HorizontalAlignment="Right" VerticalAlignment="Bottom" Style="{StaticResource Button_Custom}" Command="{Binding ExitCommand}"/> </Grid> </UserControl>
MainView.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using Whiteking_UnitTest_Updater.ViewModels; namespace Whiteking_UnitTest_Updater.Views { /// <summary> /// Interaction logic for MainView.xaml /// </summary> public partial class MainView : UserControl { public MainView() { InitializeComponent(); this.DataContext = new MainViewModel(); } } }
FileSelectControl.xaml
<UserControl x:Class="Whiteking_UnitTest_Updater.Views.FileSelectControl" 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:Whiteking_UnitTest_Updater.Views" d:DataContext="{d:DesignInstance Type=local:FileSelectControl}" mc:Ignorable="d" d:DesignHeight="30" d:DesignWidth="300"> <Grid> <Button x:Name="FileSelectButton" Grid.Column="1" Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FileSelectControl}}, Path=SelectedFilepath, UpdateSourceTrigger=PropertyChanged}" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:FileSelectControl}}, Path=SelectFileCommand}"> <Button.Style> <Style TargetType="{x:Type Button}"> <Setter Property="OverridesDefaultStyle" Value="True"/> <Setter Property="SnapsToDevicePixels" Value="True"/> <Setter Property="Background" Value="Black"/> <Setter Property="Foreground" Value="Lime"/> <Setter Property="BorderBrush" Value="Lime"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Border x:Name="Container" BorderThickness="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <TextBlock x:Name="Content" Grid.Column="0" Padding="5,5,20,5" VerticalAlignment="Center" FontSize="{TemplateBinding FontSize}" FontWeight="{TemplateBinding FontWeight}" Text="{TemplateBinding Content}"/> <Border Grid.Column="1" BorderThickness="1,0,0,0" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding Foreground}" Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Grid}}, Path=ActualHeight}"> <Path x:Name="Search_Icon" Margin="5" Stretch="Uniform" Fill="{TemplateBinding Foreground}"> <Path.Data> M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z </Path.Data> </Path> </Border> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Lime"/> <Setter Property="Foreground" Value="Black"/> <Setter Property="BorderBrush" Value="Lime"/> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="BorderBrush" Value="Red"/> <Setter Property="Foreground" Value="Red"/> </Trigger> </Style.Triggers> </Style> </Button.Style> </Button> </Grid> </UserControl>
FileSelectControl.xaml.cs
using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using Microsoft.Win32; using Whiteking_UnitTest_Updater.Services; namespace Whiteking_UnitTest_Updater.Views { /// <summary> /// Interaction logic for FileSelectControl.xaml /// </summary> public partial class FileSelectControl : UserControl, INotifyPropertyChanged { public FileSelectControl() { InitializeComponent(); SelectFileCommand = new RelayCommand(SelectFile); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { Trace.WriteLine("PropertyChanged called, property Name: " + propertyName); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #region DependencyProperty_SelectedFilepath public string SelectedFilepath { get => (string)GetValue(SelectedFilepathProperty); set { Trace.WriteLine("SelectedFilepathProperty SET was called"); SetValue(SelectedFilepathProperty, value); NotifyPropertyChanged(); } } public static readonly DependencyProperty SelectedFilepathProperty = DependencyProperty.Register( "SelectedFilepath", typeof(string), typeof(FileSelectControl), new PropertyMetadata("")); #endregion public RelayCommand SelectFileCommand { get; private set; } private void SelectFile(object sender) { string filepath; OpenFileDialog dialogue = new OpenFileDialog(); bool? results = dialogue.ShowDialog(); if (results == true) { //Get the path of specified file filepath = dialogue.FileName; SelectedFilepath = filepath; } } } }
解决方案
问题根源有两个:
- 依赖属性的CLR包装器错误添加了
NotifyPropertyChanged调用,且未设置双向绑定默认元数据 - MainView中未显式指定双向绑定模式(或依赖属性未默认支持双向)
1. 修正FileSelectControl的依赖属性定义
WPF依赖属性的CLR包装器只是语法糖,WPF内部会直接调用GetValue/SetValue,所以包装器里的NotifyPropertyChanged不会被绑定系统触发。同时需要给依赖属性添加BindsTwoWayByDefault元数据,让绑定默认支持双向:
#region DependencyProperty_SelectedFilepath public string SelectedFilepath { get => (string)GetValue(SelectedFilepathProperty); set => SetValue(SelectedFilepathProperty, value); } public static readonly DependencyProperty SelectedFilepathProperty = DependencyProperty.Register( "SelectedFilepath", typeof(string), typeof(FileSelectControl), new FrameworkPropertyMetadata( string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); #endregion
2. 确保MainView的绑定配置(可选,若已设置默认双向可省略)
显式给SelectedFilepath绑定添加Mode=TwoWay,让绑定逻辑更清晰:
<local:FileSelectControl Height="30" Width="500" SelectedFilepath="{Binding SelectedFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
3. 验证ViewModel的SelectedFile属性实现
确保MainViewModel中的SelectedFile正确实现INotifyPropertyChanged,否则ViewModel的变更无法同步到UI:
public class MainViewModel : INotifyPropertyChanged { private string _selectedFile; public string SelectedFile { get => _selectedFile; set { if (_selectedFile != value) { _selectedFile = value; OnPropertyChanged(); } } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // 其他命令实现... }
内容的提问来源于stack exchange,提问作者TylerWright

