4
\$\begingroup\$

I've created a UserControl that should act as a control inside a Window:

XAML:

    <UserControl x:Class="MyApplication.UserControls.DateTimePicker"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:converters="clr-namespace:MyApplication.Converters"
             x:Name="dtp">
    <UserControl.Resources>
        <converters:NullDateTimeConverter x:Key="nullDateTimeConverter" />
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="AUTO" />
        </Grid.ColumnDefinitions>
        <TextBox HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 Text="{Binding Path=SelectedDateTime,
                                ElementName=dtp,
                                Converter={StaticResource nullDateTimeConverter},
                                StringFormat={}{0:MM/dd/yyyy HH:mm},
                                UpdateSourceTrigger=PropertyChanged}" />
        <Button Grid.Row="0"
                Grid.Column="1"
                Width="50"
                BorderBrush="Orange"
                BorderThickness="1"
                Command="{Binding ChangePopupStatusCommand}"
                Style="{DynamicResource SquareButtonStyle}">
            <Rectangle Width="20"
                       Height="20"
                       Fill="{Binding RelativeSource={RelativeSource AncestorType=Button},
                                      Path=Foreground}">
                <Rectangle.OpacityMask>
                    <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_calendar}" />
                </Rectangle.OpacityMask>
            </Rectangle>
        </Button>

        <Popup Grid.Row="1"
               Grid.Column="0"
               Width="400"
               IsOpen="{Binding IsPopupOpen}"
               PopupAnimation="Slide">
            <DockPanel LastChildFill="True">
                <Grid DockPanel.Dock="Top">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="2*" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <RepeatButton Command="{Binding ChangeTimeCommand}"
                                  CommandParameter="addHour"
                                  Content="+" />
                    <RepeatButton Grid.Column="1"
                                  Command="{Binding ChangeTimeCommand}"
                                  CommandParameter="addMinute"
                                  Content="+" />
                    <TextBox
                             Grid.Row="1"
                             Grid.ColumnSpan="2"
                             HorizontalContentAlignment="Center"
                             VerticalContentAlignment="Center">
                        <TextBox.Text>
                            <MultiBinding StringFormat=" {0:D2}:{1:D2}">
                                <Binding Mode="OneWay" Path="SelectedDateTime.Hour" />
                                <Binding Mode="OneWay" Path="SelectedDateTime.Minute" />
                            </MultiBinding>
                        </TextBox.Text>
                    </TextBox>
                    <RepeatButton Grid.Row="2"
                                  Command="{Binding ChangeTimeCommand}"
                                  CommandParameter="subHour"
                                  Content="-" />
                    <!--  Click="SubHours_Click"  -->
                    <RepeatButton Grid.Row="2"
                                  Grid.Column="1"
                                  Command="{Binding ChangeTimeCommand}"
                                  CommandParameter="subMinute"
                                  Content="-" />
                </Grid>
                <Calendar DockPanel.Dock="Bottom"
                          SelectedDate="{Binding SelectedDateTime,
                                                 ElementName=dtp,
                                                 Converter={StaticResource nullDateTimeConverter}}"
                          Style="{StaticResource styleCalendar}" />
            </DockPanel>
        </Popup>
    </Grid>
</UserControl>

XAML.CS

public partial class DateTimePicker : UserControl, INotifyPropertyChanged
{
    public DateTimePicker()
    {
        InitializeComponent();
        (Content as FrameworkElement).DataContext = this;
    }

    #region SelectedTime

    public DateTime? SelectedDateTime
    {
        get
        {
            return (DateTime?)GetValue(SelectedDateTimeProperty);
        }
        set
        {
            SetValue(SelectedDateTimeProperty, value);
        }
    }

    public static readonly DependencyProperty 
        SelectedDateTimeProperty =
        DependencyProperty.Register("SelectedDateTime",
        typeof(DateTime?),
        typeof(DateTimePicker));

    #endregion

    private bool _isPopupOpen;
    public bool IsPopupOpen
    {
        get { return _isPopupOpen; }
        set
        {
            SetProperty(ref _isPopupOpen, value);
        }
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    protected virtual void SetProperty<T>(ref T member, T val,
       [CallerMemberName]string propertyName = null)
    {
        if (object.Equals(member, val)) return;

        member = val;
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion INotifyPropertyChanged

    #region ChangePopup Command
    private CommandBase _changePopupStatusCommand;
    public CommandBase ChangePopupStatusCommand
    {
        get { return _changePopupStatusCommand ?? (_changePopupStatusCommand = new CommandBase(ChangePopupStatus)); }
    }
    private void ChangePopupStatus(object obj)
    {
        IsPopupOpen = !IsPopupOpen;
    }
    #endregion

    #region ChangeTime Command
    private CommandBase _chageTimeCommand;
    public CommandBase ChangeTimeCommand
    {
        get { return _chageTimeCommand ?? (_chageTimeCommand = new CommandBase(ChangeTime)); }
    }
    private void ChangeTime(object obj)
    {
        if (!SelectedDateTime.HasValue)
            SelectedDateTime = DateTime.Now;
        switch (obj.ToString())
        {
            case "addHour":
                SelectedDateTime = SelectedDateTime.Value.AddHours(1);
                break;
            case "addMinute":
                SelectedDateTime = SelectedDateTime.Value.AddMinutes(1);
                break;
            case "subHour":
                SelectedDateTime = SelectedDateTime.Value.AddHours(-1);
                break;
            case "subMinute":
                SelectedDateTime = SelectedDateTime.Value.AddMinutes(-1);
                break;
        }
    }
    #endregion
}

NullDateTimeConverter:

public class NullDateTimeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var val = (Nullable<DateTime>)value;

        if (val.HasValue && val.Value > DateTime.MinValue)
            return val;

        return String.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string strValue = value.ToString();
        DateTime resultDateTime;
        return DateTime.TryParse(strValue, out resultDateTime) ? resultDateTime : value;
    }
}

Usage:

<DockPanel>
    <TextBlock Text="Calibration Due" />
    <uc:DateTimePicker SelectedDateTime="{Binding CalibarationDue, 
                Mode=TwoWay, 
                UpdateSourceTrigger=PropertyChanged}" />
</DockPanel>

The thing I'm most unsure of is the way I use the binding in side my picker. Any comments or suggestions?

\$\endgroup\$
  • \$\begingroup\$ I would suggest building a custom control. user control's though can accomplish the same result's are conventionally used to group existing elements with no logic. further more using a custom control will enable you to switch the control template with something else. and refer to your control with TemplateBinding from within it's template. \$\endgroup\$ – eran otzap Aug 16 '15 at 4:52
3
\$\begingroup\$

You have used two types of bindings:

1. Binding between your control and the usage DataContext

This binding is fine and is the way how WPF binding system was supposed to work with respect to the MVVM pattern.

2. Bindings between xaml controls and the xaml class (Same class which is the parent of those controls)

The interactions between xaml controls and xaml code-behind are not supposed to flow through the same MVVM pattern. This is why in the partial class you have the possibility to access directly your controls.

Example from xaml:

<Button Grid.Row="0"
            Grid.Column="1"
            Width="50"
            BorderBrush="Orange"
            BorderThickness="1"
            Name="ButtonChangeStatus"
            Click="ButtonChangeStatus_OnClick"
            Style="{DynamicResource SquareButtonStyle}">

Example from code-behind:

void ButtonChangeStatus_OnClick(object sender, RoutedEventArgs e)
{
    IsPopupOpen = !IsPopupOpen;
    ButtonChangeStatus.Background = Brushes.Green;
}

Another drawback could be the usage of some properties from outside of your implementation. Suppose there is some logic which needs to open the popup without a click on the ButtonChangeStatus button but from a click which originates outside of your implementation. To achieve such behavior the user of your control would need to bind IsOpen to his DataContext. But in the current implementation it's not possible without a great confusion (you need to have two IsOpen dependency properties); it will be possible by exposing the IsOpen dependency property to the outside ViewModel in the same way as you already did with SelectedDateTime. Having both a local IsOpen property and a dependency one will allow you to handle the Popup using two sources.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.