0

This is a simple datetime control that has the added feature of minutes and hours.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;

namespace foo.WizardElements
{

/// <summary>
/// Interaction logic for DateTimeRangeElement.xaml
/// </summary>
public partial class DateTimeRangeElement : UserControl
{
    public DateTimeRangeElement()
    {
        InitializeComponent();
        dp.DataContext = this;
    }

    private void Clear_Click(object sender, RoutedEventArgs e)
    {
        Date = null;
        Hours = 0;
        Minutes = 0;
    }
    public static readonly DependencyProperty DateProperty = DependencyProperty.Register("Date",
                                                                                        typeof(DateTime?),
                                                                                        typeof(DateTimeRangeElement));

    public DateTime? Date
    {
        get { return (DateTime?)GetValue(DateProperty); }
        set 
        { 
            SetValue(DateProperty, value);
        }
    }

    public static readonly DependencyProperty HoursProperty = DependencyProperty.Register("Hours",
                                                                                typeof(int),
                                                                                typeof(DateTimeRangeElement));

    public int Hours
    {
        get { return (int)GetValue(HoursProperty); }
        set
        {
            SetValue(HoursProperty, value);
        }
    }

    public static readonly DependencyProperty MinutesProperty = DependencyProperty.Register("Minutes",
                                                                                typeof(int),
                                                                                typeof(DateTimeRangeElement));

    public int Minutes
    {
        get { return (int)GetValue(MinutesProperty); }
        set
        {
            SetValue(MinutesProperty, value);
        }
    }

    private void TimeUpdated()
    {
        if(Hours > 23)
            Hours = 23;
        if(Minutes > 59)
            Minutes = 59;
        if (Date.HasValue && (Date.Value.Hour != Hours || Date.Value.Minute != Minutes))
        {
            Date = new DateTime(Date.Value.Year, Date.Value.Month, Date.Value.Day, Hours, Minutes, 0);
        }
        if ((!Date.HasValue) && (Hours > 0 || Minutes > 0))
        {
            var now = DateTime.Now;
            Date = new DateTime(now.Year, now.Month, now.Day, Hours, Minutes, 0);
        }
    }

    private void Changed(object sender, object e)
    {
        TimeUpdated();
    }

}
}

and the xaml

<UserControl x:Class="foo.WizardElements.DateTimeRangeElement"
             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:i="http://schemas.microsoft.com/expression/2010/interactivity" 
             xmlns:behavior="clr-namespace:foo.Behaviours"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             x:Name="myDateTimeControl">
    <Grid>
        <StackPanel Orientation="Horizontal" Margin="2" Height="24">
            <DatePicker x:Name="dp" 
                        VerticalAlignment="top" 
                        Foreground="LightGray" 
                        SelectedDate="{Binding ElementName=myDateTimeControl,Path=Date, Mode=TwoWay}" 
                        BorderBrush="{x:Null}" SelectedDateChanged="Changed"
                        >
                <DatePicker.Background>
                    <SolidColorBrush Color="white" Opacity="0.2"/>
                </DatePicker.Background>
            </DatePicker>
            <TextBox Width="30"  Text="{Binding ElementName=myDateTimeControl, Path=Hours, Mode=TwoWay, StringFormat=00}" TextChanged="Changed">
                <i:Interaction.Behaviors>
                    <behavior:AllowableCharactersTextBoxBehavior RegularExpression="^[0-9]*$" MaxLength="3" />
                </i:Interaction.Behaviors>
            </TextBox>
            <Label Content=":"/>

            <TextBox Width="30" Text="{Binding ElementName=myDateTimeControl, Path=Minutes, Mode=TwoWay, StringFormat=00}" TextChanged="Changed">
                <i:Interaction.Behaviors>
                    <behavior:AllowableCharactersTextBoxBehavior RegularExpression="^[0-9]*$" MaxLength="3" />
                </i:Interaction.Behaviors>
            </TextBox>
            <Grid  HorizontalAlignment="Left" VerticalAlignment="top">
                <Button 
                    Background="Transparent" 
                    Click="Clear_Click" 
                    BorderThickness="0"
                    >
                    <Grid>
                        <Image Source="/foo;component/Images/Clear-left.png" Stretch="Uniform"/>
                    </Grid>
                </Button>
            </Grid>
        </StackPanel>
    </Grid>
</UserControl>

So here is the usecase, you put this puppy on a tab, select a time & date, navigate off of the tab and return. If you only databind the "Date" proeprty and not the hours and minutes than you find that both of those are set to 0.

The reason for this is that once you navigate off of a tab you dispose of the user controll and when you navigate back you greate a new user controll (.Net 4).

Binding to the Hours and minutes is fugly, and does not amke sense for the consumer as it expects a DateTime object.

I'm tring to figure out what the corect pattern would be for making the hours and minutes reload when the usercontrol gets recreated.

this is how the usercontrol is used in the application

<we:DateTimeRangeElement Date="{Binding Path=Filter.StartTime, Mode=TwoWay}" />

EDIT: I have a solution, that I don't like, but it will do untill I can get the glue out of the way. what I did was create my own datetime object, and useing that I was able to get a more bindable object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using ToolSuite.Contract.BaseClasses;
using System.Globalization;

namespace foo.WizardElements
{
    /// <summary>
    /// Interaction logic for DateTimeRangeElement.xaml
    /// </summary>
    public partial class DateTimeRangeElement : UserControl
    {
        public DateTimeRangeElement()
        {
            InitializeComponent();
            this.GotFocus += DateTimeRangeElement_GotFocus;
        }

        void DateTimeRangeElement_GotFocus(object sender, RoutedEventArgs e)
        {
            if(Date!=null)
                Date.PropertyChanged += Date_PropertyChanged;
        }

        void Date_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            this.GetBindingExpression(DateProperty).UpdateSource();
        }

        private void Clear_Click(object sender, RoutedEventArgs e)
        {
            Date = null;
        }
        public static readonly DependencyProperty DateProperty = DependencyProperty.Register("Date",
                                                                                            typeof(FriendlyDateTime),
                                                                                            typeof(DateTimeRangeElement));

        public FriendlyDateTime Date
        {
            get { return (FriendlyDateTime)GetValue(DateProperty); }
            set 
            {
                SetValue(DateProperty, value);
            }
        }
    }
}

the xaml

    <UserControl x:Class="foo.WizardElements.DateTimeRangeElement"
             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:i="http://schemas.microsoft.com/expression/2010/interactivity" 
             xmlns:behavior="clr-namespace:foo.Behaviours"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             x:Name="myDateTimeControl">
    <Grid>
        <StackPanel Orientation="Horizontal" Margin="2" Height="24">
            <DatePicker x:Name="dp" 
                        VerticalAlignment="top" 
                        Foreground="LightGray" 
                        SelectedDate="{Binding ElementName=myDateTimeControl,Path=Date.Date, Mode=TwoWay}" 
                        BorderBrush="{x:Null}"
                        >
                <DatePicker.Background>
                    <SolidColorBrush Color="white" Opacity="0.2"/>
                </DatePicker.Background>
            </DatePicker>
            <TextBox x:Name="Hours" Width="30"  Text="{Binding ElementName=myDateTimeControl, Path=Date.Hour, Mode=TwoWay, StringFormat=00}">
                <i:Interaction.Behaviors>
                    <behavior:AllowableCharactersTextBoxBehavior RegularExpression="^[0-9]*$" MaxLength="3" />
                </i:Interaction.Behaviors>
            </TextBox>
            <Label Content=":"/>

            <TextBox x:Name="Minutes" Width="30" Text="{Binding ElementName=myDateTimeControl, Path=Date.Minute, Mode=TwoWay, StringFormat=00}">
                <i:Interaction.Behaviors>
                    <behavior:AllowableCharactersTextBoxBehavior RegularExpression="^[0-9]*$" MaxLength="3" />
                </i:Interaction.Behaviors>
            </TextBox>
            <Grid  HorizontalAlignment="Left" VerticalAlignment="top">
                <Button 
                    Background="Transparent" 
                    Click="Clear_Click" 
                    BorderThickness="0"
                    >
                    <Grid>
                        <Image Source="/foo;component/Images/Clear-left.png" Stretch="Uniform"/>
                    </Grid>
                </Button>
            </Grid>
        </StackPanel>
    </Grid>
</UserControl>

the helper

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ToolSuite.Contract.BaseClasses;

namespace foo.WizardElements
{
    public class FriendlyDateTime : NotifyPropertyChangedBase
    {
        public FriendlyDateTime()
        {

        }
        public FriendlyDateTime(DateTime? value)
        {
            Date = value;
        }

        public DateTime? Date
        {
            get
            {
                if (base._values.ContainsKey("Date"))
                    return Get<DateTime>("Date");
                else
                    return null;
            }

            set
            {
                if (value == null)
                {
                    if (base._values.ContainsKey("Date"))
                        base._values.Remove("Date");
                }
                else
                    Set<DateTime>("Date", value.Value);

                base.NotifyPropertyChanged("Date");
                base.NotifyPropertyChanged("Hour");
                base.NotifyPropertyChanged("Minute");
            }
        }
        public int Hour
        {
            get { return Date.HasValue ? Date.Value.Hour : 0; }
            set
            {
                if (Hour > 23)
                    Hour = 23;
                var d = Date.HasValue ? Date.Value : DateTime.Now;
                Date = new DateTime(d.Year, d.Month, d.Day, value, Minute, 0);
            }
        }
        public int Minute
        {
            get { return Date.HasValue ? Date.Value.Minute : 0; }
            set
            {
                if (Minute > 59)
                    Minute = 59;
                var d = Date.HasValue ? Date.Value : DateTime.Now;
                Date = new DateTime(d.Year, d.Month, d.Day, Hour, value, 0);
            }
        }

        static public implicit operator DateTime?(FriendlyDateTime value)
        {
            return value.Date;
        }

        static public implicit operator FriendlyDateTime(DateTime? value)
        {
            // Note that because RomanNumeral is declared as a struct, 
            // calling new on the struct merely calls the constructor 
            // rather than allocating an object on the heap:
            return new FriendlyDateTime(value);
        }
    }
}

the somewhat useless thurd that I'd like to get rid of

    using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Globalization;
using ToolSuite.Contract.BaseClasses;

namespace foo.WizardElements
{
    public class FriendlyDateTimeValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType == typeof(DateTime?))
            {
                return ((FriendlyDateTime)value).Date;
            }
            else if (targetType == typeof(FriendlyDateTime))
            {
                return new FriendlyDateTime(value as DateTime?);
            }
            return null;
        }



        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (targetType == typeof(DateTime?))
            {
                return ((FriendlyDateTime)value).Date;
            }
            else if (targetType == typeof(FriendlyDateTime))
            {
                return new FriendlyDateTime(value as DateTime?);
            }
            return null;
        }

    }
}

and lastly the way it is used

                <we:DateTimeRangeElement Date="{Binding Path=Filter.EndTime, Mode=TwoWay, Converter={StaticResource DateTimeConverter}, UpdateSourceTrigger=Explicit}" />

Do I like it? No, not really I wish that I could ditch the value converter and the explicit binding. but that will be a research project for an other day.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Bas Hamer
  • 304
  • 3
  • 16

3 Answers3

2

The solution is MVVM pattern. Your binded data has not to have any relation to a control. Even if you navigate away from the Tab and content controls are disposed, the data remains.

EDIT

I see that in XAML you bind to the properties of your user control. This fundamentaly wrong. You need to consume your data from DataLayer and not from the control. Create a class that is used like a value holder for those properties.

Tigran
  • 61,654
  • 8
  • 86
  • 123
  • the properties are bindable, and that is how they are being used. so there is a object that has a datetime property that is set up w/ 2 way binding. The hours and minutes are causing the issues. – Bas Hamer Jan 13 '12 at 20:47
  • @Bas Harmer: as much I see, they are the properties of `UserControl` so if the control disposes for any reason, the properties gone. – Tigran Jan 13 '12 at 20:49
  • yeah I did not show how the conrol was being used; but if you amke the properties bindable than the data persists in the boudn to value. I was hoping to maintain the same pattern as the datepicker control, but with hours and minutes. – Bas Hamer Jan 13 '12 at 20:54
  • @Bas Hamer: But if you have a class binded to `UserControl` that should hold the valid data. May be your issure, at this point, is that you need rebind your data, so it will be pushed from data layer on UI. – Tigran Jan 13 '12 at 21:08
  • the data is in the bound DateTime object, that object is on a controller, no issues there. the only problem is that the display for the hours and minutes resets to 00 between navigations. I know that there are solutions like writing your own datetime class that has read/write properties for the hours and minutes, but I'd like it to work w/ a vanilla Datetime if I can and hold that as a last resort. – Bas Hamer Jan 13 '12 at 21:23
0

You bind int hour and int min by:

    public DateTime StartTime
    {
        get { return startdate; }
        set
        {
            startdate = new DateTime(value.Year, value.Month, value.Day, startdate.Hour, startdate.Minute, 0);
            RaisePropertyChanged("StartTime");
            RaisePropertyChanged("StartHour");
            RaisePropertyChanged("StartMinute");
        }
    }

    public int StartHour
    {
        get { return StartTime.Hour; }
        set
        {
            startdate = new DateTime(startdate.Year, startdate.Month, startdate.Day, value, startdate.Minute, 0);

        }
    }

same for min.... At least that's what I did before with using MVVM, but all these is contains in a data object in my ViewModel.

King Chan
  • 4,212
  • 10
  • 46
  • 78
  • yep, you effectively created a custom datetime object rahter than the out of the box one, swapping the readonly properties of Hour and Minute for readwrite properties. that will work but i was hoping to find a way to do it w/ the vanilla datetime object. – Bas Hamer Jan 13 '12 at 21:24
0

It depends on if this is a generic UserControl meant to be used by many different applications, or a one-time UserControl meant to be used in one specific case.

If it's a generic control, why not bind Hours/Minutes to the bound Date property? To do that, just bind to DateTimeRangeElement.Date.Hours (or Minutes) instead of DateTimeRangeElement.Hours. If the user is binding a single date data object, they would expect the hours/minutes of that object to get updated when they change the values in your control anyways.

If you don't want to do this, then its up to the user to DataBind Hours/Minutes if they want to keep the value from resetting. It's kind of like using any other UserControl with a TabControl - CheckBox.IsChecked, ListBox.SelectedItem, Expander.IsExpanded, etc all get lost unless they're bound to something.

If this is a one-time UserControl, meant to be used with a specific View and TabControl which you have control over, then just be sure to bind the Hours / Minutes.

The other alternative I've used in the past is to overwrite the TabControl to cache the ContentPresenter when the tab gets unloaded, and to use the cached ContentPresenter instead of re-loading the tab item when it gets switched back. If you want the code for that, it's located in this answer to another question

Community
  • 1
  • 1
Rachel
  • 130,264
  • 66
  • 304
  • 490
  • this causes issues as DateTime does not implement INotifyPropertyChanged, so a change to the date does not propagate to the hours. – Bas Hamer Jan 13 '12 at 20:56
  • @BasHamer `DependencyProperty.Register` provides an overload that will let you register a `PropertyChanged` method, although you shouldn't need it since your UI will be bound to the `Date` property and will update when it changes (you can get rid of the `Hours/Minutes` properties altogether. If people care for them, they can use `Date.Hours` or `Date.Minutes`) – Rachel Jan 13 '12 at 21:05
  • I'd like to only code this once; and it is already used a few times. updated teh binding to {Binding ElementName=myDateTimeControl, Path=Date.Hour, Mode=OneWay, StringFormat=00} and pulled out the hour and minute proeprty, reading the text in teh filed on the changed event. The value is persisted between tab navigations, but the display still turns to 00 – Bas Hamer Jan 13 '12 at 21:19