Search code examples
c#wpfdata-bindingdependency-properties

How can I Two-Way bind the dependency property of a custom UserControl to the property of another UserControl's DataContext?


I have a UserControl who's code-behind has declared a dependency property. I also have another UserControl who is serving as the main UI.

The main UI usercontrol has set its DataContext to a viewmodel and that viewmodel has a property SelectedFile. The XAML for the main UI usercontrol declares another usercontrol and binds the dependency property "SelectedFilePath" to the property SelectedFile.

I would like to keep the UserControl FileSelectControl encapsulated from the UserControl MainView. Ideally FileSelectControl can contain all the code it needs to select a file and pass the required info on to MainWindow. So I need the child control to pass its data to the parent control.

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

My code builds without any error, The binding even works one-way, I can edit a textbox in the main UI usercontrol and it sets the dependancy property and the changes are visualized as I would expect.

The problem, is that when the UserControl with the dependency property sets its the property on its own through a RelayCommand, this doesn't bubble up to the parent USerControl and change the bound property.

I'm pretty stumped, and I've tried fudging the code around... to no avail.

I was hoping someone could help with this. I would like my UserControl "FileSelectControl" to be able to trigger the selectedFilePath property to change when a file is selected and have that affect bound parameters in the parent.


Solution

  • The binding in the MainView should be TwoWay:

    <local:FileSelectControl Height="30" Width="500"
        SelectedFilepath="{Binding SelectedFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    

    Alternatively, you can define the dependency property to bind two-way by default in the FileSelectControl:

    public static readonly DependencyProperty SelectedFilepathProperty = DependencyProperty.Register(
        "SelectedFilepath",
        typeof(string),
        typeof(FileSelectControl),
        new FrameworkPropertyMetadata("") { BindsTwoWayByDefault = true });
    

    Also note that there is no reason for the control to implement INotifyPropertyChanged when it sets a dependency property. This implementation is cleaner and should work just fine:

    public partial class FileSelectControl : UserControl
    {
        public FileSelectControl()
        {
            InitializeComponent();
            SelectFileCommand = new RelayCommand(SelectFile);
        }
    
        #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("") { BindsTwoWayByDefault = true  });
        #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;
            }
        }
    }