Browse Source

Merge pull request #1367 from sdoroff/date-picker-control

DatePicker control
Steven Kirk 7 years ago
parent
commit
24f83fe3b8

+ 6 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -60,6 +60,9 @@
     <EmbeddedResource Include="Pages\DropDownPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Pages\DatePickerPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <EmbeddedResource Include="Pages\ExpanderPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
@@ -128,6 +131,9 @@
     <Compile Include="Pages\DropDownPage.xaml.cs">
       <DependentUpon>DropDownPage.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\DatePickerPage.xaml.cs">
+      <DependentUpon>DatePickerPage.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\ExpanderPage.xaml.cs">
       <DependentUpon>ExpanderPage.xaml</DependentUpon>
     </Compile>

+ 2 - 1
samples/ControlCatalog/MainView.xaml

@@ -7,11 +7,12 @@
     </TabControl.Transition>
     <TabItem Header="Border"><pages:BorderPage/></TabItem>
     <TabItem Header="Button"><pages:ButtonPage/></TabItem>
-    <TabItem Header="Calendar"><pages:CalendarPage/></TabItem>
+    <TabItem Header="Calendar"><pages:CalendarPage/></TabItem> 
     <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
     <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
     <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
     <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
+    <TabItem Header="DatePicker"><pages:DatePickerPage/></TabItem>
     <TabItem Header="DropDown"><pages:DropDownPage/></TabItem>
     <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
     <TabItem Header="Image"><pages:ImagePage/></TabItem>

+ 46 - 0
samples/ControlCatalog/Pages/DatePickerPage.xaml

@@ -0,0 +1,46 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">DatePicker</TextBlock>
+    <TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
+        
+    <StackPanel Orientation="Horizontal"
+                Margin="0,16,0,0"
+                HorizontalAlignment="Center"
+                Gap="16">
+      <StackPanel Orientation="Vertical"
+                  Width="200">
+        <TextBlock Text="SelectedDateFormat: Short"/>
+        <DatePicker Name="DatePicker1"
+                    SelectedDateFormat="Short"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="SelectedDateFormat: Long"/>
+        <DatePicker Name="DatePicker2"
+                    SelectedDateFormat="Long"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="SelectedDateFormat: Custom"/>
+        <DatePicker Name="DatePicker3"
+                    SelectedDateFormat="Custom"
+                    CustomDateFormatString="ddd, MMM d"
+                    Margin="0,0,0,8"/>
+
+        <TextBlock Text="Blackout Dates"/>
+        <DatePicker Name="DatePicker4"
+                    Margin="0,0,0,8"/>
+
+        <DatePicker Margin="0,0,0,8"
+                    Watermark="Watermark"/>
+        <DatePicker Margin="0,0,0,8"
+                    Name="DatePicker5"
+                    Watermark="Floating Watermark"
+                    UseFloatingWatermark="True"/>
+                
+        <TextBlock Text="Disabled"/>
+        <DatePicker IsEnabled="False"/>
+      </StackPanel>
+
+    </StackPanel> 
+  </StackPanel>
+</UserControl>

+ 36 - 0
samples/ControlCatalog/Pages/DatePickerPage.xaml.cs

@@ -0,0 +1,36 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using System;
+
+namespace ControlCatalog.Pages
+{
+    public class DatePickerPage : UserControl
+    {
+        public DatePickerPage()
+        {
+            InitializeComponent();
+            
+            var dp1 = this.FindControl<DatePicker>("DatePicker1");
+            var dp2 = this.FindControl<DatePicker>("DatePicker2");
+            var dp3 = this.FindControl<DatePicker>("DatePicker3");
+            var dp4 = this.FindControl<DatePicker>("DatePicker4");
+            var dp5 = this.FindControl<DatePicker>("DatePicker5");
+
+            dp1.SelectedDate = DateTime.Today;
+            dp2.SelectedDate = DateTime.Today.AddDays(10);
+            dp3.SelectedDate = DateTime.Today.AddDays(20);
+            dp5.SelectedDate = DateTime.Today;
+
+            dp4.TemplateApplied += (s, e) =>
+            {
+                dp4.BlackoutDates.AddDatesInPast();
+            };
+            
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 1188 - 0
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -0,0 +1,1188 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the
+    /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+    /// event.
+    /// </summary>
+    public class DatePickerDateValidationErrorEventArgs : EventArgs
+    {
+        private bool _throwException;
+
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" />
+        /// class.
+        /// </summary>
+        /// <param name="exception">
+        /// The initial exception from the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </param>
+        /// <param name="text">
+        /// The text that caused the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </param>
+        public DatePickerDateValidationErrorEventArgs(Exception exception, string text)
+        {
+            this.Text = text;
+            this.Exception = exception;
+        }
+
+        /// <summary>
+        /// Gets the initial exception associated with the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <value>
+        /// The exception associated with the validation failure.
+        /// </value>
+        public Exception Exception { get; private set; }
+
+        /// <summary>
+        /// Gets the text that caused the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <value>
+        /// The text that caused the validation failure.
+        /// </value>
+        public string Text { get; private set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether
+        /// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" />
+        /// should be thrown.
+        /// </summary>
+        /// <value>
+        /// True if the exception should be thrown; otherwise, false.
+        /// </value>
+        /// <exception cref="T:System.ArgumentException">
+        /// If set to true and
+        /// <see cref="P:Avalonia.Controls.DatePickerDateValidationErrorEventArgs.Exception" />
+        /// is null.
+        /// </exception>
+        public bool ThrowException
+        {
+            get { return this._throwException; }
+            set
+            {
+                if (value && this.Exception == null)
+                {
+                    throw new ArgumentException("Cannot Throw Null Exception");
+                }
+                this._throwException = value;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Specifies date formats for a
+    /// <see cref="T:Avalonia.Controls.DatePicker" />.
+    /// </summary>
+    public enum DatePickerFormat
+    {
+        /// <summary>
+        /// Specifies that the date should be displayed using unabbreviated days
+        /// of the week and month names.
+        /// </summary>
+        Long = 0,
+
+        /// <summary>
+        /// Specifies that the date should be displayed using abbreviated days
+        /// of the week and month names.
+        /// </summary>
+        Short = 1,
+
+        /// <summary>
+        /// Specifies that the date should be displayed using a custom format string.
+        /// </summary>
+        Custom = 2
+    }
+
+    public class DatePicker : TemplatedControl
+    {
+        private const string ElementTextBox = "PART_TextBox";
+        private const string ElementButton = "PART_Button";
+        private const string ElementPopup = "PART_Popup";
+        private const string ElementCalendar = "PART_Calendar";
+
+        private Calendar _calendar;
+        private string _defaultText;
+        private Button _dropDownButton;
+        //private Canvas _outsideCanvas;
+        //private Canvas _outsidePopupCanvas;
+        private Popup _popUp;
+        private TextBox _textBox;
+        private IDisposable _textBoxTextChangedSubscription;
+        private IDisposable _buttonPointerPressedSubscription;
+
+        private DateTime? _onOpenSelectedDate;
+        private bool _settingSelectedDate;
+
+        private DateTime _displayDate;
+        private DateTime? _displayDateStart;
+        private DateTime? _displayDateEnd;
+        private bool _isDropDownOpen;
+        private DateTime? _selectedDate;
+        private string _text;
+        private bool _suspendTextChangeHandler = false;
+        private bool _isPopupClosing = false;
+        private bool _ignoreButtonClick = false;
+
+        /// <summary>
+        /// Gets a collection of dates that are marked as not selectable.
+        /// </summary>
+        /// <value>
+        /// A collection of dates that cannot be selected. The default value is
+        /// an empty collection.
+        /// </value>
+        public CalendarBlackoutDatesCollection BlackoutDates { get; private set; }
+
+        public static readonly DirectProperty<DatePicker, DateTime> DisplayDateProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime>(
+                nameof(DisplayDate),
+                o => o.DisplayDate,
+                (o, v) => o.DisplayDate = v);
+        public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateStartProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(DisplayDateStart),
+                o => o.DisplayDateStart,
+                (o, v) => o.DisplayDateStart = v);
+        public static readonly DirectProperty<DatePicker, DateTime?> DisplayDateEndProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(DisplayDateEnd),
+                o => o.DisplayDateEnd,
+                (o, v) => o.DisplayDateEnd = v);
+        public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
+            AvaloniaProperty.Register<DatePicker, DayOfWeek>(nameof(FirstDayOfWeek));
+
+        public static readonly DirectProperty<DatePicker, bool> IsDropDownOpenProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, bool>(
+                nameof(IsDropDownOpen),
+                o => o.IsDropDownOpen,
+                (o, v) => o.IsDropDownOpen = v);
+
+        public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
+            AvaloniaProperty.Register<DatePicker, bool>(nameof(IsTodayHighlighted));
+        public static readonly DirectProperty<DatePicker, DateTime?> SelectedDateProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, DateTime?>(
+                nameof(SelectedDate),
+                o => o.SelectedDate,
+                (o, v) => o.SelectedDate = v);
+
+        public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty =
+            AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
+                nameof(SelectedDateFormat),
+                defaultValue: DatePickerFormat.Short,
+                validate: ValidateSelectedDateFormat);
+
+        public static readonly StyledProperty<string> CustomDateFormatStringProperty =
+            AvaloniaProperty.Register<DatePicker, string>(
+                nameof(CustomDateFormatString),
+                defaultValue: "d",
+                validate: ValidateDateFormatString);
+
+        public static readonly DirectProperty<DatePicker, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<DatePicker, string>(
+                nameof(Text),
+                o => o.Text,
+                (o, v) => o.Text = v);
+        public static readonly StyledProperty<string> WatermarkProperty =
+            TextBox.WatermarkProperty.AddOwner<DatePicker>();
+        public static readonly StyledProperty<bool> UseFloatingWatermarkProperty =
+            TextBox.UseFloatingWatermarkProperty.AddOwner<DatePicker>();
+
+
+        /// <summary>
+        /// Gets or sets the date to display.
+        /// </summary>
+        /// <value>
+        /// The date to display. The default 
+        /// <see cref="P:System.DateTime.Today" />.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The specified date is not in the range defined by
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateStart" />
+        /// and
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateEnd" />.
+        /// </exception>
+        public DateTime DisplayDate
+        {
+            get { return _displayDate; }
+            set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); }
+        }
+        
+        /// <summary>
+        /// Gets or sets the first date to be displayed.
+        /// </summary>
+        /// <value>The first date to display.</value>
+        public DateTime? DisplayDateStart
+        {
+            get { return _displayDateStart; }
+            set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the last date to be displayed.
+        /// </summary>
+        /// <value>The last date to display.</value>
+        public DateTime? DisplayDateEnd
+        {
+            get { return _displayDateEnd; }
+            set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the day that is considered the beginning of the week.
+        /// </summary>
+        /// <value>
+        /// A <see cref="T:System.DayOfWeek" /> representing the beginning of
+        /// the week. The default is <see cref="F:System.DayOfWeek.Sunday" />.
+        /// </value>
+        public DayOfWeek FirstDayOfWeek
+        {
+            get { return GetValue(FirstDayOfWeekProperty); }
+            set { SetValue(FirstDayOfWeekProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is open or closed.
+        /// </summary>
+        /// <value>
+        /// True if the <see cref="T:Avalonia.Controls.Calendar" /> is
+        /// open; otherwise, false. The default is false.
+        /// </value>
+        public bool IsDropDownOpen
+        {
+            get { return _isDropDownOpen; }
+            set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the current date will be
+        /// highlighted.
+        /// </summary>
+        /// <value>
+        /// True if the current date is highlighted; otherwise, false. The
+        /// default is true.
+        /// </value>
+        public bool IsTodayHighlighted
+        {
+            get { return GetValue(IsTodayHighlightedProperty); }
+            set { SetValue(IsTodayHighlightedProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the currently selected date.
+        /// </summary>
+        /// <value>
+        /// The date currently selected. The default is null.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The specified date is not in the range defined by
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateStart" />
+        /// and
+        /// <see cref="P:Avalonia.Controls.DatePicker.DisplayDateEnd" />,
+        /// or the specified date is in the
+        /// <see cref="P:Avalonia.Controls.DatePicker.BlackoutDates" />
+        /// collection.
+        /// </exception>
+        public DateTime? SelectedDate
+        {
+            get { return _selectedDate; }
+            set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the format that is used to display the selected date.
+        /// </summary>
+        /// <value>
+        /// The format that is used to display the selected date. The default is
+        /// <see cref="F:Avalonia.Controls.DatePickerFormat.Short" />.
+        /// </value>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// An specified format is not valid.
+        /// </exception>
+        public DatePickerFormat SelectedDateFormat
+        {
+            get { return GetValue(SelectedDateFormatProperty); }
+            set { SetValue(SelectedDateFormatProperty, value); }
+        }
+
+        public string CustomDateFormatString
+        {
+            get { return GetValue(CustomDateFormatStringProperty); }
+            set { SetValue(CustomDateFormatStringProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the text that is displayed by the
+        /// <see cref="T:Avalonia.Controls.DatePicker" />.
+        /// </summary>
+        /// <value>
+        /// The text displayed by the
+        /// <see cref="T:Avalonia.Controls.DatePicker" />.
+        /// </value>
+        /// <exception cref="T:System.FormatException">
+        /// The text entered cannot be parsed to a valid date, and the exception
+        /// is not suppressed.
+        /// </exception>
+        /// <exception cref="T:System.ArgumentOutOfRangeException">
+        /// The text entered parses to a date that is not selectable.
+        /// </exception>
+        public string Text
+        {
+            get { return _text; }
+            set { SetAndRaise(TextProperty, ref _text, value); }
+        }
+
+        public string Watermark
+        {
+            get { return GetValue(WatermarkProperty); }
+            set { SetValue(WatermarkProperty, value); }
+        }
+        public bool UseFloatingWatermark
+        {
+            get { return GetValue(UseFloatingWatermarkProperty); }
+            set { SetValue(UseFloatingWatermarkProperty, value); }
+        }
+
+        /// <summary>
+        /// Occurs when the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is closed.
+        /// </summary>
+        public event EventHandler CalendarClosed;
+
+        /// <summary>
+        /// Occurs when the drop-down
+        /// <see cref="T:Avalonia.Controls.Calendar" /> is opened.
+        /// </summary>
+        public event EventHandler CalendarOpened;
+
+        /// <summary>
+        /// Occurs when <see cref="P:Avalonia.Controls.DatePicker.Text" />
+        /// is assigned a value that cannot be interpreted as a date.
+        /// </summary>
+        public event EventHandler<DatePickerDateValidationErrorEventArgs> DateValidationError;
+
+        /// <summary>
+        /// Occurs when the
+        /// <see cref="P:Avalonia.Controls.DatePicker.SelectedDate" />
+        /// property is changed.
+        /// </summary>
+        public event EventHandler<SelectionChangedEventArgs> SelectedDateChanged;
+
+        static DatePicker()
+        {
+            FocusableProperty.OverrideDefaultValue<DatePicker>(true);
+
+            DisplayDateProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateChanged);
+            DisplayDateStartProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateStartChanged);
+            DisplayDateEndProperty.Changed.AddClassHandler<DatePicker>(x => x.OnDisplayDateEndChanged);
+            IsDropDownOpenProperty.Changed.AddClassHandler<DatePicker>(x => x.OnIsDropDownOpenChanged);
+            SelectedDateProperty.Changed.AddClassHandler<DatePicker>(x => x.OnSelectedDateChanged);
+            SelectedDateFormatProperty.Changed.AddClassHandler<DatePicker>(x => x.OnSelectedDateFormatChanged);
+            CustomDateFormatStringProperty.Changed.AddClassHandler<DatePicker>(x => x.OnCustomDateFormatStringChanged);
+            TextProperty.Changed.AddClassHandler<DatePicker>(x => x.OnTextChanged);
+        }
+        /// <summary>
+        /// Initializes a new instance of the
+        /// <see cref="T:Avalonia.Controls.DatePicker" /> class.
+        /// </summary>
+        public DatePicker()
+        {
+            FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;
+            _defaultText = string.Empty;
+            DisplayDate = DateTime.Today;
+        }
+
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                _calendar.DayButtonMouseUp -= Calendar_DayButtonMouseUp;
+                _calendar.DisplayDateChanged -= Calendar_DisplayDateChanged;
+                _calendar.SelectedDatesChanged -= Calendar_SelectedDatesChanged;
+                _calendar.PointerPressed -= Calendar_PointerPressed;
+                _calendar.KeyDown -= Calendar_KeyDown;
+            }
+            _calendar = e.NameScope.Find<Calendar>(ElementCalendar);
+            if (_calendar != null)
+            {
+                _calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+                _calendar.SelectedDate = SelectedDate;
+                SetCalendarDisplayDate(DisplayDate);
+                SetCalendarDisplayDateStart(DisplayDateStart);
+                SetCalendarDisplayDateEnd(DisplayDateEnd);
+
+                _calendar.DayButtonMouseUp += Calendar_DayButtonMouseUp;
+                _calendar.DisplayDateChanged += Calendar_DisplayDateChanged;
+                _calendar.SelectedDatesChanged += Calendar_SelectedDatesChanged;
+                _calendar.PointerPressed += Calendar_PointerPressed;
+                _calendar.KeyDown += Calendar_KeyDown;
+                //_calendar.SizeChanged += new SizeChangedEventHandler(Calendar_SizeChanged);
+                //_calendar.IsTabStop = true;
+
+                var currentBlackoutDays = BlackoutDates;
+                BlackoutDates = _calendar.BlackoutDates;
+                if(currentBlackoutDays != null)
+                {
+                    foreach (var range in currentBlackoutDays)
+                    {
+                        BlackoutDates.Add(range);
+                    }
+                }
+            }
+
+            if (_popUp != null)
+            {
+                _popUp.Child = null;
+                _popUp.Closed -= PopUp_Closed;
+            }
+            _popUp = e.NameScope.Find<Popup>(ElementPopup);
+            if(_popUp != null)
+            {
+                _popUp.Closed += PopUp_Closed;
+                if (IsDropDownOpen)
+                {
+                    OpenDropDown();
+                }
+            }
+
+            if(_dropDownButton != null)
+            {
+                _dropDownButton.Click -= DropDownButton_Click;
+                _buttonPointerPressedSubscription?.Dispose();
+            }
+            _dropDownButton = e.NameScope.Find<Button>(ElementButton);
+            if(_dropDownButton != null)
+            {
+                _dropDownButton.Click += DropDownButton_Click;
+                _buttonPointerPressedSubscription =
+                    _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
+            }
+
+            if (_textBox != null)
+            {
+                _textBox.KeyDown -= TextBox_KeyDown;
+                _textBox.GotFocus -= TextBox_GotFocus;
+                _textBoxTextChangedSubscription?.Dispose();
+            }
+            _textBox = e.NameScope.Find<TextBox>(ElementTextBox);
+
+            if(!SelectedDate.HasValue)
+            {
+                SetWaterMarkText();
+            }
+
+            if(_textBox != null)
+            {
+                _textBox.KeyDown += TextBox_KeyDown;
+                _textBox.GotFocus += TextBox_GotFocus;
+                _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBox_TextChanged());
+
+                if(SelectedDate.HasValue)
+                {
+                    _textBox.Text = DateTimeToString(SelectedDate.Value);
+                }
+                else if(!String.IsNullOrEmpty(_defaultText))
+                {
+                    _textBox.Text = _defaultText;
+                    SetSelectedDate();
+                }
+            }
+
+            base.OnTemplateApplied(e);
+        }
+
+        protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
+        {
+            if (property == SelectedDateProperty)
+            {
+                DataValidationErrors.SetError(this, status.Error);
+            }
+        }
+
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+            if (!e.Handled && SelectedDate.HasValue && _calendar != null)
+            {
+                DateTime selectedDate = this.SelectedDate.Value;
+                DateTime? newDate = DateTimeHelper.AddDays(selectedDate, e.Delta.Y > 0 ? -1 : 1);
+                if (newDate.HasValue && Calendar.IsValidDateSelection(_calendar, newDate.Value))
+                {
+                    SelectedDate = newDate;
+                    e.Handled = true;
+                }
+            }
+        }
+        protected override void OnGotFocus(GotFocusEventArgs e)
+        {
+            base.OnGotFocus(e);
+            if(IsEnabled && _textBox != null && e.NavigationMethod == NavigationMethod.Tab)
+            {
+                _textBox.Focus();
+                var text = _textBox.Text;
+                if(!string.IsNullOrEmpty(text))
+                {
+                    _textBox.SelectionStart = 0;
+                    _textBox.SelectionEnd = text.Length;
+                }
+            }
+        }
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+
+            SetSelectedDate();
+        }
+        
+        private void SetCalendarDisplayDate(DateTime value)
+        {
+            if (DateTimeHelper.CompareYearMonth(_calendar.DisplayDate, value) != 0)
+            {
+                _calendar.DisplayDate = DisplayDate;
+                if (DateTime.Compare(_calendar.DisplayDate, DisplayDate) != 0)
+                {
+                    DisplayDate = _calendar.DisplayDate;
+                }
+            }
+        }
+        private void OnDisplayDateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime)e.NewValue;
+                SetCalendarDisplayDate(value);
+            }
+        }
+        private void SetCalendarDisplayDateStart(DateTime? value)
+        {
+            _calendar.DisplayDateStart = value;
+            if (_calendar.DisplayDateStart.HasValue && DisplayDateStart.HasValue && DateTime.Compare(_calendar.DisplayDateStart.Value, DisplayDateStart.Value) != 0)
+            {
+                DisplayDateStart = _calendar.DisplayDateStart;
+            }
+        }
+        private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime?)e.NewValue;
+                SetCalendarDisplayDateStart(value);
+            }
+        }
+        private void SetCalendarDisplayDateEnd(DateTime? value)
+        {
+            _calendar.DisplayDateEnd = value;
+            if (_calendar.DisplayDateEnd.HasValue && DisplayDateEnd.HasValue && DateTime.Compare(_calendar.DisplayDateEnd.Value, DisplayDateEnd.Value) != 0)
+            {
+                DisplayDateEnd = _calendar.DisplayDateEnd;
+            }
+
+        }
+        private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (_calendar != null)
+            {
+                var value = (DateTime?)e.NewValue;
+                SetCalendarDisplayDateEnd(value);
+            }
+        }
+        private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (bool)e.OldValue;
+            var value = (bool)e.NewValue;
+
+            if (_popUp != null && _popUp.Child != null)
+            {
+                if (value != oldValue)
+                {
+                    if (_calendar.DisplayMode != CalendarMode.Month)
+                    {
+                        _calendar.DisplayMode = CalendarMode.Month;
+                    }
+
+                    if (value)
+                    {
+                        OpenDropDown();
+                    }
+                    else
+                    {
+                        _popUp.IsOpen = false;
+                        OnCalendarClosed(new RoutedEventArgs());
+                    }
+                }
+            }
+        }
+        private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var addedDate = (DateTime?)e.NewValue;
+            var removedDate = (DateTime?)e.OldValue;
+
+            if (_calendar != null && addedDate != _calendar.SelectedDate)
+            {
+                _calendar.SelectedDate = addedDate;
+            }
+
+            if (SelectedDate != null)
+            {
+                DateTime day = SelectedDate.Value;
+
+                // When the SelectedDateProperty change is done from
+                // OnTextPropertyChanged method, two-way binding breaks if
+                // BeginInvoke is not used:
+                Threading.Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    _settingSelectedDate = true;
+                    Text = DateTimeToString(day);
+                    _settingSelectedDate = false;
+                    OnDateSelected(addedDate, removedDate);
+                });
+
+                // When DatePickerDisplayDateFlag is TRUE, the SelectedDate
+                // change is coming from the Calendar UI itself, so, we
+                // shouldn't change the DisplayDate since it will automatically
+                // be changed by the Calendar
+                if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.DatePickerDisplayDateFlag))
+                {
+                    DisplayDate = day;
+                }
+                if(_calendar != null)
+                    _calendar.DatePickerDisplayDateFlag = false;
+            }
+            else
+            {
+                _settingSelectedDate = true;
+                SetWaterMarkText();
+                _settingSelectedDate = false;
+                OnDateSelected(addedDate, removedDate);
+            }
+        }
+        private void OnDateFormatChanged()
+        {
+            if (_textBox != null)
+            {
+                if (SelectedDate.HasValue)
+                {
+                    Text = DateTimeToString(SelectedDate.Value);
+                }
+                else if (string.IsNullOrEmpty(_textBox.Text))
+                {
+                    SetWaterMarkText();
+                }
+                else
+                {
+                    DateTime? date = ParseText(_textBox.Text);
+
+                    if (date != null)
+                    {
+                        string s = DateTimeToString((DateTime)date);
+                        Text = s;
+                    }
+                }
+            }
+        }
+        private void OnSelectedDateFormatChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            OnDateFormatChanged();
+        }
+        private void OnCustomDateFormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if(SelectedDateFormat == DatePickerFormat.Custom)
+            {
+                OnDateFormatChanged();
+            }
+        }
+        private void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var oldValue = (string)e.OldValue;
+            var value = (string)e.NewValue;
+
+            if (!_suspendTextChangeHandler)
+            {
+                if (value != null)
+                {
+                    if (_textBox != null)
+                    {
+                        _textBox.Text = value;
+                    }
+                    else
+                    {
+                        _defaultText = value;
+                    }
+                    if (!_settingSelectedDate)
+                    {
+                        SetSelectedDate();
+                    }
+                }
+                else
+                {
+                    if (!_settingSelectedDate)
+                    {
+                        _settingSelectedDate = true;
+                        SelectedDate = null;
+                        _settingSelectedDate = false;
+                    }
+                }
+            }
+            else
+            {
+                SetWaterMarkText();
+            }
+        }
+
+        /// <summary>
+        /// Raises the
+        /// <see cref="E:Avalonia.Controls.DatePicker.DateValidationError" />
+        /// event.
+        /// </summary>
+        /// <param name="e">
+        /// A
+        /// <see cref="T:Avalonia.Controls.DatePickerDateValidationErrorEventArgs" />
+        /// that contains the event data.
+        /// </param>
+        protected virtual void OnDateValidationError(DatePickerDateValidationErrorEventArgs e)
+        {
+            DateValidationError?.Invoke(this, e);
+        }
+        private void OnDateSelected(DateTime? addedDate, DateTime? removedDate)
+        {
+            EventHandler<SelectionChangedEventArgs> handler = this.SelectedDateChanged;
+            if (null != handler)
+            {
+                Collection<DateTime> addedItems = new Collection<DateTime>();
+                Collection<DateTime> removedItems = new Collection<DateTime>();
+
+                if (addedDate.HasValue)
+                {
+                    addedItems.Add(addedDate.Value);
+                }
+
+                if (removedDate.HasValue)
+                {
+                    removedItems.Add(removedDate.Value);
+                }
+
+                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, addedItems, removedItems));
+            }
+        }
+        private void OnCalendarClosed(EventArgs e)
+        {
+            CalendarClosed?.Invoke(this, e);
+        }
+        private void OnCalendarOpened(EventArgs e)
+        {
+            CalendarOpened?.Invoke(this, e);
+        }
+
+        private void Calendar_DayButtonMouseUp(object sender, PointerReleasedEventArgs e)
+        {
+            Focus();
+            IsDropDownOpen = false;
+        }      
+        private void Calendar_DisplayDateChanged(object sender, CalendarDateChangedEventArgs e)
+        {
+            if (e.AddedDate != this.DisplayDate)
+            {
+                SetValue(DisplayDateProperty, (DateTime) e.AddedDate);
+            }
+        }
+        private void Calendar_SelectedDatesChanged(object sender, SelectionChangedEventArgs e)
+        {
+            Debug.Assert(e.AddedItems.Count < 2, "There should be less than 2 AddedItems!");
+
+            if (e.AddedItems.Count > 0 && SelectedDate.HasValue && DateTime.Compare((DateTime)e.AddedItems[0], SelectedDate.Value) != 0)
+            {
+                SelectedDate = (DateTime?)e.AddedItems[0];
+            }
+            else
+            {
+                if (e.AddedItems.Count == 0)
+                {
+                    SelectedDate = null;
+                    return;
+                }
+
+                if (!SelectedDate.HasValue)
+                {
+                    if (e.AddedItems.Count > 0)
+                    {
+                        SelectedDate = (DateTime?)e.AddedItems[0];
+                    }
+                }
+            }
+        }
+        private void Calendar_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            if (e.MouseButton == MouseButton.Left)
+            {
+                e.Handled = true;
+            }
+        }
+        private void Calendar_KeyDown(object sender, KeyEventArgs e)
+        {
+            Calendar c = sender as Calendar;
+            Debug.Assert(c != null, "The Calendar should not be null!");
+
+            if (!e.Handled && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape) && c.DisplayMode == CalendarMode.Month)
+            {
+                Focus();
+                IsDropDownOpen = false;
+
+                if (e.Key == Key.Escape)
+                {
+                    SelectedDate = _onOpenSelectedDate;
+                }
+            }
+        }
+        private void TextBox_GotFocus(object sender, RoutedEventArgs e)
+        {
+            IsDropDownOpen = false;
+        }
+        private void TextBox_KeyDown(object sender, KeyEventArgs e)
+        {
+            if (!e.Handled)
+            {
+                e.Handled = ProcessDatePickerKey(e);
+            }
+        }
+        private void TextBox_TextChanged()
+        {
+            if (_textBox != null)
+            {
+                _suspendTextChangeHandler = true;
+                Text = _textBox.Text;
+                _suspendTextChangeHandler = false;
+            }
+        }
+        private void DropDownButton_PointerPressed(object sender, PointerPressedEventArgs e)
+        {
+            _ignoreButtonClick = _isPopupClosing;
+        }
+        private void DropDownButton_Click(object sender, RoutedEventArgs e)
+        {
+            if (!_ignoreButtonClick)
+            {
+                HandlePopUp();
+            }
+            else
+            {
+                _ignoreButtonClick = false;
+            }
+        }
+        private void PopUp_Closed(object sender, EventArgs e)
+        {
+            IsDropDownOpen = false;
+
+            if(!_isPopupClosing)
+            {
+                _isPopupClosing = true;
+                Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
+            }
+        }
+
+        private void HandlePopUp()
+        {
+            if (IsDropDownOpen)
+            {
+                Focus();
+                IsDropDownOpen = false;
+            }
+            else
+            {
+                ProcessTextBox();
+            }
+        }
+        private void OpenDropDown()
+        {
+            if (_calendar != null)
+            {
+                _calendar.Focus();
+                OpenPopUp();
+                _calendar.ResetStates();
+                OnCalendarOpened(new RoutedEventArgs());
+            }
+        }
+        private void OpenPopUp()
+        {
+            _onOpenSelectedDate = SelectedDate;
+            _popUp.IsOpen = true;
+        }
+
+        /// <summary>
+        /// Input text is parsed in the correct format and changed into a
+        /// DateTime object.  If the text can not be parsed TextParseError Event
+        /// is thrown.
+        /// </summary>
+        /// <param name="text">Inherited code: Requires comment.</param>
+        /// <returns>
+        /// IT SHOULD RETURN NULL IF THE STRING IS NOT VALID, RETURN THE
+        /// DATETIME VALUE IF IT IS VALID.
+        /// </returns>
+        private DateTime? ParseText(string text)
+        {
+            DateTime newSelectedDate;
+
+            // TryParse is not used in order to be able to pass the exception to
+            // the TextParseError event
+            try
+            {
+                newSelectedDate = DateTime.Parse(text, DateTimeHelper.GetCurrentDateFormat());
+
+                if (Calendar.IsValidDateSelection(this._calendar, newSelectedDate))
+                {
+                    return newSelectedDate;
+                }
+                else
+                {
+                    var dateValidationError = new DatePickerDateValidationErrorEventArgs(new ArgumentOutOfRangeException("text", "SelectedDate value is not valid."), text);
+                    OnDateValidationError(dateValidationError);
+
+                    if (dateValidationError.ThrowException)
+                    {
+                        throw dateValidationError.Exception;
+                    }
+                }
+            }
+            catch (FormatException ex)
+            {
+                DatePickerDateValidationErrorEventArgs textParseError = new DatePickerDateValidationErrorEventArgs(ex, text);
+                OnDateValidationError(textParseError);
+
+                if (textParseError.ThrowException)
+                {
+                    throw textParseError.Exception;
+                }
+            }
+            return null;
+        }
+        private string DateTimeToString(DateTime d)
+        {
+            DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
+
+            switch (SelectedDateFormat)
+            {
+                case DatePickerFormat.Short:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.ShortDatePattern, dtfi));
+                case DatePickerFormat.Long:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(dtfi.LongDatePattern, dtfi));
+                case DatePickerFormat.Custom:
+                    return string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormatString, dtfi));
+            }
+            return null;
+        }
+        private bool ProcessDatePickerKey(KeyEventArgs e)
+        {
+            
+            switch (e.Key)
+            {
+                case Key.Enter:
+                    {
+                        SetSelectedDate();
+                        return true;
+                    }
+                case Key.Down:
+                    { 
+                        if ((e.Modifiers & InputModifiers.Control) == InputModifiers.Control)
+                        {
+                            HandlePopUp();
+                            return true;
+                        }
+                        break;
+                    }
+            }
+            return false;
+        }
+        private void ProcessTextBox()
+        {
+            SetSelectedDate();
+            IsDropDownOpen = true;
+            _calendar.Focus();
+        }
+        private void SetSelectedDate()
+        {
+            if (_textBox != null)
+            {
+                if (!string.IsNullOrEmpty(_textBox.Text))
+                {
+                    string s = _textBox.Text;
+
+                    if (SelectedDate != null)
+                    {
+                        // If the string value of the SelectedDate and the
+                        // TextBox string value are equal, we do not parse the
+                        // string again if we do an extra parse, we lose data in
+                        // M/d/yy format.
+                        // ex: SelectedDate = DateTime(1008,12,19) but when
+                        // "12/19/08" is parsed it is interpreted as
+                        // DateTime(2008,12,19)
+                        string selectedDate = DateTimeToString(SelectedDate.Value);
+                        if (selectedDate == s)
+                        {
+                            return;
+                        }
+                    }
+                    DateTime? d = SetTextBoxValue(s);
+                    if (!SelectedDate.Equals(d))
+                    {
+                        SelectedDate = d;
+                    }
+                }
+                else
+                {
+                    if (SelectedDate != null)
+                    {
+                        SelectedDate = null;
+                    }
+                }
+            }
+            else
+            {
+                DateTime? d = SetTextBoxValue(_defaultText);
+                if (!SelectedDate.Equals(d))
+                {
+                    SelectedDate = d;
+                }
+            }
+        }
+        private DateTime? SetTextBoxValue(string s)
+        {
+            if (string.IsNullOrEmpty(s))
+            {
+                SetValue(TextProperty, s);
+                return SelectedDate;
+            }
+            else
+            {
+                DateTime? d = ParseText(s);
+                if (d != null)
+                {
+                    SetValue(TextProperty, s);
+                    return d;
+                }
+                else
+                {
+                    // If parse error: TextBox should have the latest valid
+                    // selecteddate value:
+                    if (SelectedDate != null)
+                    {
+                        string newtext = this.DateTimeToString(SelectedDate.Value);
+                        SetValue(TextProperty, newtext);
+                        return SelectedDate;
+                    }
+                    else
+                    {
+                        SetWaterMarkText();
+                        return null;
+                    }
+                }
+            }
+        }
+        private void SetWaterMarkText()
+        {
+            if (_textBox != null)
+            {
+                if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark)
+                {
+                    DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
+                    Text = string.Empty;
+                    _defaultText = string.Empty;
+                    var watermarkFormat = "<{0}>";
+                    string watermarkText;
+
+                    switch (SelectedDateFormat)
+                    {
+                        case DatePickerFormat.Long:
+                            {
+                                watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.LongDatePattern.ToString());
+                                break;
+                            }
+                        case DatePickerFormat.Short:
+                        default:
+                            {
+                                watermarkText = string.Format(CultureInfo.CurrentCulture, watermarkFormat, dtfi.ShortDatePattern.ToString());
+                                break;
+                            }
+                    }
+                    _textBox.Watermark = watermarkText;
+                }
+                else
+                {
+                    _textBox.ClearValue(TextBox.WatermarkProperty);
+                }
+            }
+        }
+
+        private static bool IsValidSelectedDateFormat(DatePickerFormat value)
+        {
+            return value == DatePickerFormat.Long
+                || value == DatePickerFormat.Short
+                || value == DatePickerFormat.Custom;
+        }
+        private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format)
+        {
+            if(IsValidSelectedDateFormat(format))
+            {
+                return format;
+            }
+            else
+            {
+                throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid.");
+            }
+        }
+        private static string ValidateDateFormatString(DatePicker dp, string formatString)
+        {
+            if(string.IsNullOrWhiteSpace(formatString))
+            {
+                throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString));
+            }
+            else
+            {
+                return formatString;
+            }
+        }
+        private static DateTime DiscardDayTime(DateTime d)
+        {
+            int year = d.Year;
+            int month = d.Month;
+            DateTime newD = new DateTime(year, month, 1, 0, 0, 0);
+            return newD;
+        }
+        private static DateTime? DiscardTime(DateTime? d)
+        {
+            if (d == null)
+            {
+                return null;
+            }
+            else
+            {
+                DateTime discarded = (DateTime) d;
+                int year = discarded.Year;
+                int month = discarded.Month;
+                int day = discarded.Day;
+                DateTime newD = new DateTime(year, month, day, 0, 0, 0);
+                return newD;
+            }
+        }
+    }
+}

+ 126 - 0
src/Avalonia.Themes.Default/DatePicker.xaml

@@ -0,0 +1,126 @@
+<!--
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+-->
+
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <Style Selector="DatePicker">
+
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="Padding" Value="4"/>
+    
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="*,Auto">
+
+          <Grid.Styles>
+
+            <Style Selector="Button.CalendarDropDown">
+              <Setter Property="Template">
+                <ControlTemplate>
+                  <Grid Height="18"
+                        Width="19"
+                        HorizontalAlignment="Center"
+                        VerticalAlignment="Center"
+                        Margin="0"
+                        Background="#FFFFFFFF"
+                        ColumnDefinitions="*,*,*,*"
+                        RowDefinitions="23*,19*,19*,19*"
+                        ClipToBounds="False">
+
+                    <Border Name="Highlight"
+                            Margin="-1"
+                            Grid.ColumnSpan="4"
+                            Grid.Row="0"
+                            Grid.RowSpan="4"
+                            BorderThickness="1"
+                            BorderBrush="{DynamicResource HighlightBrush}" />
+                    <Border Name="Background"
+                            Margin="0,-1,0,0"
+                            Grid.ColumnSpan="4"
+                            Grid.Row="1"
+                            Grid.RowSpan="3"
+                            BorderThickness="1"
+                            BorderBrush="{DynamicResource ThemeBorderDarkBrush}"
+                            CornerRadius=".5" />
+                    <Rectangle Grid.ColumnSpan="4"
+                               Grid.RowSpan="1"
+                               StrokeThickness="1"
+                               Stroke="{DynamicResource ThemeBorderDarkBrush}"
+                               Fill="{DynamicResource ThemeAccentBrush}">
+                    </Rectangle>
+                    <Path HorizontalAlignment="Center"
+                          Margin="4,3,4,3"
+                          VerticalAlignment="Center"
+                          RenderTransformOrigin="0.5,0.5"
+                          Grid.Column="0"
+                          Grid.Row="1"
+                          Grid.ColumnSpan="4"
+                          Grid.RowSpan="3"
+                          Fill="{DynamicResource ThemeBorderDarkBrush}"
+                          Stretch="Fill"
+                          Data="M11.426758,8.4305077 L11.749023,8.4305077 L11.749023,16.331387 L10.674805,16.331387 L10.674805,10.299648 L9.0742188,11.298672 L9.0742188,10.294277 C9.4788408,10.090176 9.9094238,9.8090878 10.365967,9.4510155 C10.82251,9.0929432 11.176106,8.7527733 11.426758,8.4305077 z M14.65086,8.4305077 L18.566387,8.4305077 L18.566387,9.3435936 L15.671368,9.3435936 L15.671368,11.255703 C15.936341,11.058764 16.27293,10.960293 16.681133,10.960293 C17.411602,10.960293 17.969301,11.178717 18.354229,11.615566 C18.739157,12.052416 18.931622,12.673672 18.931622,13.479336 C18.931622,15.452317 18.052553,16.438808 16.294415,16.438808 C15.560365,16.438808 14.951641,16.234707 14.468243,15.826504 L14.881817,14.929531 C15.368796,15.326992 15.837872,15.525723 16.289043,15.525723 C17.298809,15.525723 17.803692,14.895514 17.803692,13.635098 C17.803692,12.460618 17.305971,11.873379 16.310528,11.873379 C15.83071,11.873379 15.399232,12.079271 15.016094,12.491055 L14.65086,12.238613 z" />
+
+                    <Ellipse HorizontalAlignment="Center" VerticalAlignment="Center" Fill="#FFFFFFFF" StrokeThickness="0" Grid.ColumnSpan="4" Width="3" Height="3"/>
+                  </Grid>
+                </ControlTemplate>
+              </Setter>
+            </Style>
+
+            <Style Selector="Button.CalendarDropDown /template/ Border#Highlight">
+              <Setter Property="IsVisible" Value="False"/>
+            </Style>
+            <Style Selector="Button.CalendarDropDown:pressed /template/ Border#Highlight">
+              <Setter Property="IsVisible" Value="True"/>
+            </Style>
+
+            <Style Selector="Button.CalendarDropDown:pointerover /template/ Border#Background">
+              <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
+            </Style>
+            
+          </Grid.Styles>
+          
+          <TextBox Name="PART_TextBox"
+                   Background="{TemplateBinding Background}" 
+                   BorderBrush="{TemplateBinding BorderBrush}" 
+                   BorderThickness="{TemplateBinding BorderThickness}" 
+                   Padding="{TemplateBinding Padding}"
+                   Watermark="{TemplateBinding Watermark}"
+                   UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}"
+                   DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
+                   Grid.Column="0"/>
+
+          <Button Name="PART_Button"
+                  Grid.Column="1"
+                  Width="20"
+                  Classes="CalendarDropDown"
+                  Foreground="{TemplateBinding Foreground}"
+                  Background="#00FFFFFF"
+                  BorderThickness="0"
+                  Margin="2,0,2,0"
+                  Padding="0"
+                  ClipToBounds="False"
+                  Focusable="False"/>
+
+          <Popup Name="PART_Popup"
+                 PlacementTarget="{TemplateBinding}"
+                 StaysOpen="False">
+            <Calendar Name="PART_Calendar"
+                      FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
+                      IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}"/>
+          </Popup>
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="DatePicker:focus /template/ TextBox#PART_TextBox">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
+  </Style>
+  
+</Styles>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -41,4 +41,5 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.CalendarDayButton.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.CalendarItem.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 130 - 0
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

@@ -0,0 +1,130 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class DatePickerTests
+    {
+        private static bool CompareDates(DateTime first, DateTime second)
+        {
+            return first.Year == second.Year &&
+                first.Month == second.Month &&
+                first.Day == second.Day;
+        }
+
+        [Fact]
+        public void SelectedDateChanged_Should_Fire_When_SelectedDate_Set()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                bool handled = false;
+                DatePicker datePicker = CreateControl();
+                datePicker.SelectedDateChanged += (s,e) =>
+                {
+                    handled = true;
+                };
+                DateTime value = new DateTime(2000, 10, 10);
+                datePicker.SelectedDate = value;
+                Threading.Dispatcher.UIThread.RunJobs();
+                Assert.True(handled);
+            }
+        }
+
+        [Fact]
+        public void Setting_Selected_Date_To_Blackout_Date_Should_Throw()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                DatePicker datePicker = CreateControl();
+                datePicker.BlackoutDates.AddDatesInPast();
+
+                DateTime goodValue = DateTime.Today.AddDays(1);
+                datePicker.SelectedDate = goodValue;
+                Assert.True(CompareDates(datePicker.SelectedDate.Value, goodValue));
+
+                DateTime badValue = DateTime.Today.AddDays(-1);
+                Assert.ThrowsAny<ArgumentOutOfRangeException>(
+                    () => datePicker.SelectedDate = badValue);
+            }
+        }
+
+        [Fact]
+        public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                DatePicker datePicker = CreateControl();
+                datePicker.SelectedDate = DateTime.Today.AddDays(5);
+
+                Assert.ThrowsAny<ArgumentOutOfRangeException>(
+                    () => datePicker.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10))));
+            }
+        }
+
+        private static TestServices Services => TestServices.MockThreadingInterface.With(
+            standardCursorFactory: Mock.Of<IStandardCursorFactory>());
+
+        private DatePicker CreateControl()
+        {
+            var datePicker =
+                new DatePicker
+                {
+                    Template = CreateTemplate()
+                };
+
+            datePicker.ApplyTemplate();
+            return datePicker;
+        }
+
+        private IControlTemplate CreateTemplate()
+        {
+            return new FuncControlTemplate<DatePicker>(control =>
+            {
+                var textBox = 
+                    new TextBox
+                    {
+                        Name = "PART_TextBox"
+                    };
+                var button =
+                    new Button
+                    {
+                        Name = "PART_Button"
+                    };
+                var calendar =
+                    new Calendar
+                    {
+                        Name = "PART_Calendar"
+                    };
+                var popup =
+                    new Popup
+                    {
+                        Name = "PART_Popup"
+                    };
+
+                var panel = new Panel();
+                panel.Children.Add(textBox);
+                panel.Children.Add(button);
+                panel.Children.Add(popup);
+                panel.Children.Add(calendar);
+
+                return panel;
+            });
+
+        }
+    }
+}