Раскадровка шаблона элемента управления, установите значение в другом элементе управления в пределах того же шаблона
Меня попросили создать хак для уже существующего элемента управления DateTimePicker. Обычно в окне выбора даты / времени есть замечательное изображение календаря, а затем текстовое поле рядом с ним для отображения фактической даты. Пользователь может щелкнуть изображение и представить ему всплывающий календарь, а после выбора дата обновляется в области текстового поля.
У меня проблема в том, что другие дизайнеры не любят графику календаря и хотят просто элемент управления текстовым полем, но если пользователь дважды щелкает, чтобы открыть всплывающий календарь, получить дату и обновить ее. Я очень близок к этому благодаря другой помощи, как выяснено здесь на S/O.
Итак, чтобы описать мой шаблон контроля и сохранить его простым (не пользовательский контроль, если мне не нужны предложения). Шаблон элемента управления основан на элементе управления текстового поля. У нас есть граница с PART_ContentHost для текстового поля, а затем всплывающее окно стандартного элемента управления календаря.
Для триггеров шаблона элемента управления у меня есть один объект, связанный с ScrollViewer (область ввода текстового поля) с его событием MouseDoubleClick. Если срабатывает, устанавливает IsOpen всплывающего окна в true и выставляет календарь. Это отлично работает.
Теперь, чтобы закончить это. Если пользователь выбирает дату из календаря, следующий триггер закрывает всплывающее окно (IsOpen имеет значение false). Это тоже работает.
Моя проблема. Я также хочу при выборе взять выбранную дату и получить ее представление даты ToString(), помещенное в ScrollViewer.Content (x:Name="PART_ContentHost).
<ControlTemplate TargetType="{x:Type TextBox}" x:Key="CTTextBox" >
<StackPanel>
<Border x:Name="targetBorder"
BorderBrush="{TemplateBinding BorderBrush}"
SnapsToDevicePixels="true">
<ScrollViewer x:Name="PART_ContentHost"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
Foreground="{TemplateBinding Foreground}" />
</Border>
<Popup PlacementTarget="{Binding ElementName=PART_ContentHost}" x:Name="PopCal">
<Calendar x:Name="ActualCalendar"/>
</Popup>
</StackPanel>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="ScrollViewer.MouseDoubleClick" SourceName="PART_ContentHost">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="PopCal"
Storyboard.TargetProperty="(Popup.IsOpen)">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Calendar.SelectedDatesChanged" SourceName="ActualCalendar">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="PopCal"
Storyboard.TargetProperty="(Popup.IsOpen)">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
WHAT WOULD I PUT HERE to have the selected date of the popup calendar
inserted into the content of the PART_ContentHost...
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="PART_ContentHost"
Storyboard.TargetProperty="(Content)">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value=" ????? "/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style TargetType="{x:Type TextBox}" x:Key="STextBox" >
<!--<Setter Property="OverridesDefaultStyle" Value="True"/>-->
<Setter Property="FontFamily" Value="Arial" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Height" Value="20" />
<Setter Property="Width" Value="100" />
<Setter Property="VerticalContentAlignment" Value="Bottom" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalAlignment" Value="Left" />
<!-- Padding is Left, Top, Right, Bottom -->
<Setter Property="Padding" Value="2,0,0,2" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="Visibility" Value="Visible" />
<Setter Property="IsEnabled" Value="True" />
<Setter Property="CharacterCasing" Value="Upper" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Background" Value="White" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Template" Value="{StaticResource CTTextBox}" />
</Style>
1 ответ
Я уверен, что проблему можно решить несколькими способами, но в этих ситуациях я обычно делаю прикрепленное поведение. Но пока ситуация не совсем нормальная, потому что реализован шаблон для контроля. В любом случае, я думаю, что прикрепленное поведение подойдет для этого случая, на вашем месте я бы так и сделал.
Прикрепленное поведение является очень мощным и удобным решением, которое полностью удовлетворяет шаблону MVVM, который также может использоваться в Blend (с предопределенным интерфейсом). Прикрепленное поведение - это присоединенное свойство, которое имеет обработчик событий для изменения этого свойства, и вся логика реализована в этом обработчике.
Прежде чем начать понимать поведение, я рассмотрю некоторые изменения, которые я внес в ваш шаблон.
Я немного не понимаю, почему вы используете как PART_ContentHost
Элемент управления ScrollViewer, возможно, будет несколько дат и их нужно будет отображать с помощью прокрутки. В WPF есть два элемента управления, которые необходимы для отображения содержимого:
- ContentPresenter
- ContentControl
это их главная цель. Первый самый светлый обычно всегда используется в шаблонах, но он не поддерживает события, которые нам нужны для координации работы, поэтому я выбрал ContentControl
, По мелочи добавлены обязательные свойства для шаблона, установлено для Popup
:
AllowsTransparency="True"
VerticalOffset="4"
HorizontalOffset="-5"
Для лучшей визуализации. Теперь перейдем к примеру поведения.
XAML
<Window.Resources>
<ControlTemplate x:Key="CTTextBox" TargetType="{x:Type TextBox}">
<StackPanel AttachedBehaviors:SelectDateBehavior.IsEnabled="True"> <!-- Here is determined behaviour -->
<Border x:Name="targetBorder"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
TextBlock.Foreground="{TemplateBinding Foreground}"
SnapsToDevicePixels="True">
<ContentControl x:Name="ContentHost"
Content="{TemplateBinding Text}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="4,0,0,0" />
</Border>
<Popup x:Name="PopCal"
AllowsTransparency="True"
VerticalOffset="4"
HorizontalOffset="-5"
PlacementTarget="{Binding ElementName=ContentHost}">
<Calendar x:Name="ActualCalendar" />
</Popup>
</StackPanel>
</ControlTemplate>
<Style TargetType="{x:Type TextBox}" x:Key="STextBox">
<Setter Property="FontFamily" Value="Arial" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Height" Value="25" />
<Setter Property="Width" Value="100" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="CharacterCasing" Value="Upper" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="Background" Value="AliceBlue" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Template" Value="{StaticResource CTTextBox}" />
</Style>
</Window.Resources>
<Grid>
<TextBox Style="{StaticResource STextBox}"
Text="Select date" />
</Grid>
Atatched Behavior
public class SelectDateBehavior
{
#region IsEnabled Dependency Property
public static readonly DependencyProperty IsEnabledProperty;
public static void SetIsEnabled(DependencyObject DepObject, bool value)
{
DepObject.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(DependencyObject DepObject)
{
return (bool)DepObject.GetValue(IsEnabledProperty);
}
static SelectDateBehavior()
{
IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(SelectDateBehavior),
new UIPropertyMetadata(false, IsEnabledChanged));
}
#endregion
#region IsEnabledChanged Handler
private static void IsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Panel panel = sender as Panel;
if (panel != null)
{
if (e.NewValue is bool && ((bool)e.NewValue) == true)
{
panel.Loaded += new RoutedEventHandler(panelLoaded);
}
else
{
panel.Loaded -= new RoutedEventHandler(panelLoaded);
}
}
}
#endregion
#region Panel Loaded Handler
private static void panelLoaded(object sender, RoutedEventArgs e)
{
Panel panel = sender as Panel;
Border border = panel.FindName("targetBorder") as Border;
ContentControl contentHost = border.FindName("ContentHost") as ContentControl;
Popup popup = panel.FindName("PopCal") as Popup;
if (popup != null)
{
Calendar calendar = popup.FindName("ActualCalendar") as Calendar;
calendar.SelectedDatesChanged += new EventHandler<SelectionChangedEventArgs>(calendarSelectedDatesChanged);
}
if (contentHost != null)
{
contentHost.MouseDoubleClick += new MouseButtonEventHandler(contentHostMouseDoubleClick);
}
}
#endregion
#region ContentHost MouseDoubleClick Handler
private static void contentHostMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
ContentControl contentHost = sender as ContentControl;
Border border = contentHost.Parent as Border;
Panel panel = border.Parent as Panel;
Popup popup = panel.FindName("PopCal") as Popup;
if (popup != null)
{
popup.IsOpen = true;
}
}
#endregion
#region Calendar SelectedDatesChanged Handler
private static void calendarSelectedDatesChanged(object sender, SelectionChangedEventArgs e)
{
Calendar calendar = sender as Calendar;
Popup popup = calendar.Parent as Popup;
Panel panel = popup.Parent as Panel;
Border border = panel.FindName("targetBorder") as Border;
ContentControl contentHost = border.FindName("ContentHost") as ContentControl;
if (popup != null)
{
contentHost.Content = calendar.SelectedDate;
popup.IsOpen = false;
}
}
#endregion
}
Output
Установка текущей даты осуществляется здесь:
private static void calendarSelectedDatesChanged(object sender, SelectionChangedEventArgs e)
{
// Skipped a few lines of code
if (popup != null)
{
contentHost.Content = calendar.SelectedDate;
popup.IsOpen = false;
}
}
Some notes
Позвольте мне обратить внимание на некоторые особенности. Во-первых, нам пришлось избавиться от EvenTrigger Storyboard, потому что в анимации WPF с наивысшими значениями настроек приоритета это означает, что если однажды мы установим значение IsOpen
в анимации доступ из других источников (код и т. д.) невозможен. Поэтому у меня были все триггеры / события, оставленные на стороне поведения.
Во-вторых, решение тесно связано со структурой шаблона и управления. Это означает, что если вам нужно изменить структуру шаблона, вам придется изменить и поведение (вероятно, не сильно).
Этот пример доступен здесь.