DEV Community

Daniel Leinweber
Daniel Leinweber

Posted on

How to create a WPF DateRangePicker?

I needed a way to let a user select a date range for a WPF DataGrid filter. After some research I was not able to find a control that was capable of what I wanted, so I decided to create my own WPF User Control that allows either selecting a single date or a date range.

The finished WPF User Control can be found in my GitHub profile under XamlDateRangePicker.

The following article will describe the creation process and how to use the control.

Planning

First I needed to plan what parts the user control would need to provide the functionality I was looking for. So I did a quick paper prototype.

User Control Paper Prototype

Parts

  1. TextBox
    I wanted to show the selected date (range) similar to a DateTimePicker, so I figured I would need a Textbox.

  2. Image Button
    A button to open the calendar should be combined with the TextBox. I figured a ToggleButton would be the best choice for this functionality, because of its IsChecked state.

  3. Popup
    I wanted the calendar to slide down from the TextBox when the user clicked the ToggleButton, so I decided to add a Popup.

  4. Calendar
    The actual calendar control that is nested in the Popup.

Creating a WPF User Control

A typical WPF user control consists of two files.

  • a .xaml file
  • and a .xaml.cs file

The controls UI will be created in the .xaml file and it's logic in the .xaml.cs file.

Creating the UI

First I added a WPF User Control to my Project to create the two needed files. Then I started to edit the DateRangePicker.xaml file and added the needed controls.

<UserControl x:Class="UserControls.DateRangePicker"
             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:UserControls"
             mc:Ignorable="d" 
             x:Name="this"
             d:DesignHeight="30" d:DesignWidth="160">

    <StackPanel>
        <Border
            BorderThickness="1"
            BorderBrush="{Binding ElementName=DateRangePicker_TextBox, Path=BorderBrush}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="30" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="30" />
                </Grid.ColumnDefinitions>
                <TextBox
                    x:Name="DateRangePicker_TextBox"
                    Grid.Column="0"
                    Margin="0"
                    MinHeight="30"
                    BorderThickness="0"
                    d:Text="14.03.2023 - 20.03.2023"
                    Text="{Binding ElementName=this, Path=DisplayValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Delay=300}"
                    KeyUp="Handle_Escape_Button" />
                <ToggleButton
                    x:Name="DateRangePicker_ArrowButton"
                    Grid.Column="1"
                    Height="{Binding ElementName=DateRangePicker_TextBox, Path=Height}"
                    Focusable="False"
                    ClickMode="Press"
                    IsChecked="{Binding ElementName=this, Path=IsDropDownOpen, Mode=TwoWay}"
                    MinWidth="30"
                    Background="{Binding ElementName=DateRangePicker_TextBox, Path=Background}"
                    BorderBrush="{Binding ElementName=DateRangePicker_TextBox, Path=Background}"
                    BorderThickness="0"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Stretch">
                    <Path
                        x:Name="DateRangePicker_Arrow"
                        Data="M 0 0 L 4 4 L 8 0 Z">
                        <Path.Fill>
                            <SolidColorBrush Color="#FF444444"/>
                        </Path.Fill>
                    </Path>
                </ToggleButton>
            </Grid>
        </Border>
        <Popup
            x:Name="DateRangePicker_Popup"
            Placement="Bottom"
            IsOpen="{Binding ElementName=this, Path=IsDropDownOpen}"
            AllowsTransparency="True"
            Focusable="False"
            PopupAnimation="Slide">

            <Viewbox
                x:Name="DateRangePicker_DropDown"
                SnapsToDevicePixels="True"
                Stretch="Fill">
                <Calendar
                    x:Name="DateRangePicker_Calendar"
                    SelectionMode="SingleRange"
                    SelectedDatesChanged="DateRangePicker_Calendar_SelectedDatesChanged"/>
            </Viewbox>
        </Popup>
    </StackPanel>
</UserControl>
Enter fullscreen mode Exit fullscreen mode

To make sure the controls are binding to the code behind (.xaml.cs) I gave my user control the x:Name="this". So I could use it for example in the Text binding of the TextBox.

Text="{Binding ElementName=this, Path=DisplayValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Delay=300}"
Enter fullscreen mode Exit fullscreen mode

This will search for a DisplayValue property in my DateRangePicker.xaml.cs file.

Creating the controls logic

Then I started editing the .xaml.cs file to add the user controls logic. Here I defined EventHandler and Properties that are used in the XAML of the control, as well as a DependencyProperty that should be accessible from the outside to be able to receive the selected date (range).

Defining a Dependency Property

A dependency property can be bound from the outside of the control. You will need to register a property as a DependencyProperty like in the following code snippet.

public static readonly DependencyProperty SelectedDateRangeProperty
    = DependencyProperty.Register(
        nameof(SelectedDateRange),
        typeof(ObservableCollection<DateTime>),
        typeof(DateRangePicker),
        new FrameworkPropertyMetadata(default(ObservableCollection<DateTime>), FrameworkPropertyMetadataOptions.None));

public ObservableCollection<DateTime> SelectedDateRange
{
    get => (ObservableCollection<DateTime>)GetValue(SelectedDateRangeProperty);
    set => SetValue(SelectedDateRangeProperty, value);
}
Enter fullscreen mode Exit fullscreen mode

Updating / Interacting with the UI

In order to update or interact with the UI from your code behind, you need to make sure that you implement the INotifyPropertyChanged interface. So that you can raise a notification when you change any property that is used in your UI.

public event PropertyChangedEventHandler PropertyChanged;

public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null)
{
    var output = false;

    if (EqualityComparer<T>.Default.Equals(backingStore, value) == false)
    {
        backingStore = value;
        onChanged?.Invoke();
        RaisePropertyChanged(propertyName);

        output = true;
    }

    return output;
}
Enter fullscreen mode Exit fullscreen mode

Then you can use the SetProperty method to raise the property change which will update the UI.

Here an example of the SelectedDates property that is updated from the Calendar controls SelectedDatesChanged event.

public ObservableCollection<DateTime> SelectedDates
{
    get => _selectedDate;
    set => SetProperty(ref _selectedDate, value, onChanged: () =>
    {
        IsDropDownOpen = false;
        if (value.Any())
        {
            if (value.Count == 1)
            {
                DisplayValue = value.First().ToString("dd.MM.yyyy");
                SelectedDateRange = new ObservableCollection<DateTime> { value.First() };
            }
            else
            {
                DisplayValue = $"{value.First():dd.MM.yyyy} - {value.Last():dd.MM.yyyy}";
                SelectedDateRange = new ObservableCollection<DateTime> { value.First(), value.Last() };
            }
        }
        else
        {
            SelectedDateRange = new ObservableCollection<DateTime>();
        }
    });
}
private ObservableCollection<DateTime> _selectedDate = new ObservableCollection<DateTime>();
Enter fullscreen mode Exit fullscreen mode

Using the control

In order to use the control you need to follow the next steps.

  1. Add the project or the user control to your solution/project.
  2. Add the namespace for the user control to your XAML
xmlns:userControls="clr-namespace:UserControls;assembly=DateRangePicker"
Enter fullscreen mode Exit fullscreen mode
  1. Use the control in your XAML
<userControls:DateRangePicker 
    SelectedDateRange="{Binding SelectedDates, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Enter fullscreen mode Exit fullscreen mode

You can bind the SelectedDateRange Property to your ViewModel. This property holds a ObservableCollection<DateTime> of either one or two dates. Either the single selected DateTime or the first and the last date of the selected date range.

Screenshot of the DateRangePicker

Top comments (0)