I decided to build my own DateTimePicker composite control that places a label, datepicker and timepicker in a frame and has a DateTime property to bind to. I'm fairly new to building components with Xamarin Forms and I ended up with something that works, but a few questions.
The first question is, is it a bad design to have a composite control that can be tapped in different places causing different things to happen? The label, datepicker, and timepicker sit horizontally in a row from left to right. If you click on the datepicker, the standard datepicker dialog opens. If you click on the timepicker the standard timepicker dialog opens.
For the next question I'll show the code.
Here is the xaml:
<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:effects="clr-namespace:Sandbox.Effects"
xmlns:yummy="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
mc:Ignorable="d"
x:Class="Sandbox.Controls.DateTimePickerViewControl">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Margin="0,0,0,4"
FontSize="16"
TextColor="#797979"
VerticalOptions="Center"
BackgroundColor="Transparent"
HorizontalOptions="Start"
x:Name="placeholderLabel" />
<yummy:PancakeView BackgroundColor="#F9F9F9"
Margin="0"
Padding="8, 2, 8, 2"
BorderColor="#D5D5D5"
BorderThickness="1"
CornerRadius="8"
Grid.Column="1"
VerticalOptions="Center">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<DatePicker
Grid.Column="0"
FontSize="16"
HeightRequest="28"
TextColor="#797979"
x:Name="datePicker"
PropertyChanged="DatePicker_PropertyChanged">
<DatePicker.Effects> <!-- the effects are to get rid of the underline -->
<effects:PickerEffect />
</DatePicker.Effects>
</DatePicker>
<TimePicker
Grid.Column="1"
FontSize="16"
HeightRequest="28"
TextColor="#797979"
x:Name="timePicker"
PropertyChanged="TimePicker_PropertyChanged">
<TimePicker.Effects> <!-- the effects are to get rid of the underline -->
<effects:PickerEffect/>
</TimePicker.Effects>
</TimePicker>
</Grid>
</yummy:PancakeView>
</Grid>
Here is the code behind:
namespace Sandbox.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class DateTimePickerViewControl : Grid
{
private bool _setDateTimeEnabled = true;
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create(nameof(Placeholder)
, typeof(string)
, typeof(DateTimePickerViewControl)
, default(string)
, BindingMode.OneWay);
public static readonly BindableProperty DateTimeProperty =
BindableProperty.Create(nameof(DateTime)
, typeof(DateTime)
, typeof(DateTimePickerViewControl)
, null
, BindingMode.TwoWay);
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
public DateTime DateTime
{
get => (DateTime)GetValue(DateTimeProperty);
set => SetValue(DateTimeProperty, value);
}
public DateTimePickerViewControl()
{
InitializeComponent();
placeholderLabel.Text = Placeholder;
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == PlaceholderProperty.PropertyName)
{
placeholderLabel.Text = Placeholder;
}
else if (propertyName == DateTimeProperty.PropertyName)
{
//this is required to set the datePicker and timePicker when the DateTime property is updated by code
try
{
//do not set the DateTime property from this method. This method is called when the DateTime property changes.
_setDateTimeEnabled = false;
datePicker.Date = DateTime.Date;
timePicker.Time = DateTime.TimeOfDay;
}
finally
{
_setDateTimeEnabled = true;
}
}
}
private void DatePicker_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == DatePicker.DateProperty.PropertyName)
SetDateTime();
}
private void TimePicker_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName)
SetDateTime();
}
private void SetDateTime()
{
if (!_setDateTimeEnabled)
return;
if (datePicker.Date == null)
return;
DateTime dt = datePicker.Date;
if (timePicker.Time != null)
dt += timePicker.Time;
this.DateTime = dt;
}
}
}
OnPropertyChanged is called when the DateTime property is updated. It sets the individual datePicker and timePicker controls, but I don't want the DatePicker_PropertyChanged or TimePicker_PropertyChanged to try to re-update the DateTime property. So, I'm using a private bool instance variable (_setDateTimeEnabled) in the OnPropertyChanged method. This doesn't feel quite right. For one thing somebody else could update the code and add something to set the datePicker.Date or the timePicker.Time without first setting the _setDateTimeEnabled to false.
Is there a better way to manage being able to update a property and have the property, in turn, update the components? And then if either of the components is updated by the user, have the code update the property, but not go back and try to re-update the components again?
If anybody has any other comments about the code above, I would love to hear them. Thanks.