You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现自定义UserControl依赖属性与父控件DataContext双向绑定?

WPF自定义控件依赖属性双向绑定失效问题

我有两个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;
            }
        }
    }
}

解决方案

问题根源有两个:

  1. 依赖属性的CLR包装器错误添加了NotifyPropertyChanged调用,且未设置双向绑定默认元数据
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 17:19:52