markmnl
markmnl

Reputation: 11426

WPF UserControl with many Controls - how to create Dependency Property that maps to many controls?

I have successfully created a UserControl with a Depedency property allowing me to bind to a single TextBox within my UserControl. However Im unsure how to go about doing this when I have many Controls within my UserControl and only want to bind to single Property (built from the values in the many controls)?

The UserControl has 3 textboxes for year, month and date I want to bind this to a single Date Property, so far I have got this:

<UserControl x:Class="MyApp.DateControl"...>
<StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBox Name="textbox_year" />
        <TextBox Name="textbox_month" />
        <TextBox Name="textbox_day" />
    </StackPanel>
</StackPanel>
</UserControl>

What do I need to add to the code behind to make the Date Property got from the three textboxes so in another container using my control can just bind to Date. I realise since my UserControl is the target I have to make a Dependency Property but it seems so complicated..

public partial class DateControl : UserControl
{
    public DateControl()
    {
        InitializeComponent();
    }

    public DateTime Date
    {
        get
        {
            DateTime dt;
            if (DateTime.TryParseExact(String.Format("{0}-{1}-{2}", this.textbox_year.Text, this.textbox_month.Text, this.textbox_day.Text), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out dt))
                return dt;
            else
                return DateTime.MinValue;
        }
    }

Upvotes: 3

Views: 2534

Answers (4)

alimbada
alimbada

Reputation: 1401

You can use the TextChanged events on the TextBoxes to set the date:

public partial class DateControl : UserControl
{
    public DateControl()
    {
        InitializeComponent();

        textbox_year.TextChanged += RecalculateDate;
        textbox_month.TextChanged += RecalculateDate;
        textbox_day.TextChanged += RecalculateDate;
    }

    private void RecalculateDate( object sender, TextChangedEventArgs e )
    {
        DateTime dt;
        if ( DateTime.TryParseExact( String.Format( "{0}-{1}-{2}", textbox_year.Text, textbox_month.Text, textbox_day.Text ), "yyyy-MM-dd", null, DateTimeStyles.None, out dt ) )
            SetValue( DateProperty, dt );
    }

    public static readonly DependencyProperty DateProperty =
        DependencyProperty.Register( "Date", typeof( DateTime ), typeof( DateControl ), new PropertyMetadata( DateTime.MinValue ) );

    public DateTime Date
    {
        get { return (DateTime)GetValue( DateProperty ); }
    }
}

XAML:

<UserControl x:Class="DateControlApp.DateControl"
         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>
    <StackPanel Orientation="Horizontal">
        <TextBox Name="textbox_year" />
        <TextBox Name="textbox_month" />
        <TextBox Name="textbox_day" />
    </StackPanel>
</StackPanel>
</UserControl>

And the container:

<StackPanel>
    <DateControlApp:DateControl x:Name="dateControl" />
    <TextBlock Text="{Binding ElementName=dateControl, Path=Date}" />
</StackPanel>

It's very simplistic of course. The rest is left as an exercise for the reader :)

Upvotes: 0

Pavlo Glazkov
Pavlo Glazkov

Reputation: 20756

I suggest using a converter to achieve what you want.

Your user control's XAML will look like this:

<UserControl x:Class="MyDateControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:my="clr-namespace:MyDateControl"
             x:Name="root">
    <UserControl.Resources>
        <my:DatePartConverter x:Key="DatePartConverter"
                              Date="{Binding ElementName=root, Path=Date}"/>
    </UserControl.Resources>

    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBox Name="textbox_year" Text="{Binding ElementName=root, Path=Date, Converter={StaticResource DatePartConverter}, ConverterParameter=year, Mode=TwoWay}"/>
            <TextBox Name="textbox_month" Text="{Binding ElementName=root, Path=Date, Converter={StaticResource DatePartConverter}, ConverterParameter=month, Mode=TwoWay}" />
            <TextBox Name="textbox_day" Text="{Binding ElementName=root, Path=Date, Converter={StaticResource DatePartConverter}, ConverterParameter=day, Mode=TwoWay}" />
        </StackPanel>
    </StackPanel>
</UserControl>

In code-behind you will have only you dependency property:

public DateTime Date {
   get { return (DateTime)GetValue(DateProperty); }
   set { SetValue(DateProperty, value); }
}

public static readonly DependencyProperty DateProperty = 
   DependencyProperty.Register("Date", typeof(DateTime), typeof(MyDateControl), 
      new FrameworkPropertyMetadata(DateTime.Now, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

And the converter will look something like this:

public class DatePartConverter : Freezable, IValueConverter
{
    public DateTime Date {
        get { return (DateTime)GetValue(DateProperty); }
        set { SetValue(DateProperty, value); }
    }

    public static readonly DependencyProperty DateProperty =
        DependencyProperty.Register("Date", typeof(DateTime), typeof(DatePartConverter), new UIPropertyMetadata(DateTime.Now));


    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        DateTime date = (DateTime)value;
        string datePartType = (string)parameter;

        string result;

        switch (datePartType) {
            case "year":
                result = date.Year.ToString().PadLeft(4, '0');
                break;
            case "month":
                result = date.Month.ToString().PadLeft(2, '0');
                break;
            case "day":
                result = date.Day.ToString().PadLeft(2, '0');
                break;
            default:
                throw new InvalidOperationException("Unknown date part type (ConverterParameter)");
        }

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        string datePartValue = (string)value;
        string datePartType = (string)parameter;

        DateTime result;

        switch (datePartType) {
            case "year":
                result = new DateTime(int.Parse(datePartValue), Date.Month, Date.Day);
                break;
            case "month":
                result = new DateTime(Date.Year, int.Parse(datePartValue), Date.Day);
                break;
            case "day":
                result = new DateTime(Date.Year, Date.Month, int.Parse(datePartValue));
                break;
            default:
                throw new InvalidOperationException("Unknown date part type (ConverterParameter)");
        }

        return result;
    }

    protected override Freezable CreateInstanceCore() {
        return new DatePartConverter();
    }
}

Upvotes: 6

Prince Ashitaka
Prince Ashitaka

Reputation: 8773

If the case you mentioned is for DateTime, then you can go for Masked TextBox.

WPF Masked Textbox with a value that does not contain mask

Upvotes: 0

Thomas
Thomas

Reputation: 3358

This is how I personally would approach something like this. Normally, I would move the members out into a separate logic class and include some other validation in the setters.

public partial class MainWindow : Window, INotifyPropertyChanged
{
  public MainWindow()
  {
     InitializeComponent();
     DataContext = this;
  }

  public DateTime Date
  {
     get
     {
        DateTime dt;
        if (DateTime.TryParseExact(String.Format("{0}-{1}-{2}", DateYear, DateMonth, DateDay), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out dt))
           return dt;
        return DateTime.MinValue;
     }
  }

  private string year = "2011";
  public String DateYear
  {
     get { return year; }
     set { if (year == value) return; year = value; NotifyPropertyChanged("DateYear"); NotifyPropertyChanged("Date"); }
  }

  private string month = "11";
  public String DateMonth
  {
     get { return month; }
     set { if (month == value) return; month = value; NotifyPropertyChanged("DateMonth"); NotifyPropertyChanged("Date"); }
  }

  private string day = "11";
  public String DateDay
  {
     get { return day; }
     set { if (day == value) return; day = value; NotifyPropertyChanged("DateDay"); NotifyPropertyChanged("Date"); }
  }

  #region INotifyPropertyChanged
  public event PropertyChangedEventHandler PropertyChanged;
  private void NotifyPropertyChanged(string info)
  {
     if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(info));
  }
  #endregion
}

And the xaml

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Label>Date:</Label>
    <Label Grid.Column="1" Content="{Binding Path=Date}" />
    <Label Grid.Row="1">Year</Label>
    <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=DateYear}" />
    <Label Grid.Row="2">Month</Label>
    <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=DateMonth}" />
    <Label Grid.Row="3">Day</Label>
    <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Path=DateDay}" />
  </Grid>
</Window>

Upvotes: 0

Related Questions