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.
Parts
TextBox
I wanted to show the selected date (range) similar to a DateTimePicker, so I figured I would need a Textbox.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 itsIsChecked
state.Popup
I wanted the calendar to slide down from the TextBox when the user clicked the ToggleButton, so I decided to add a Popup.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>
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}"
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);
}
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;
}
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>();
Using the control
In order to use the control you need to follow the next steps.
- Add the project or the user control to your solution/project.
- Add the namespace for the user control to your XAML
xmlns:userControls="clr-namespace:UserControls;assembly=DateRangePicker"
- Use the control in your XAML
<userControls:DateRangePicker
SelectedDateRange="{Binding SelectedDates, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
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.
Top comments (0)