The TimeSpanUpDown (Extended WPF Toolkit) seems to have a display bug when the number of days changes from 0 to >0.
Here is a simple way to reproduce it:
<Window x:Class="TimeSpanBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
Height="100" Width="200">
<StackPanel>
<!-- Bind both to the same TimeSpan property in the ViewModel -->
<xctk:TimeSpanUpDown Value="{Binding TimeSpan}"/>
<xctk:TimeSpanUpDown Value="{Binding TimeSpan}"/>
</StackPanel>
</Window>
Enter a time span close to but below 24h. The number of days is automatically hidden.
Then press the up-arrow on the first control to increase the time span to >24h. The control now updates its display to include the number of days. The second control receives the property changed notification and also tries to update, but ends up in a weird state:
Obviously, this is a bug and should be fixed by Xceed, but does anyone know a quick and easy fix or workaround?
Why not roll your own? Welcome to custom control authoring! A simple starting point without the above bug, which you can customize to your heart's content:
[TemplatePart(Name = "UP", Type = typeof(ButtonBase))]
[TemplatePart(Name = "DOWN", Type = typeof(ButtonBase))]
public class TimeSpinner : Control
{
static TimeSpinner()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TimeSpinner), new FrameworkPropertyMetadata(typeof(TimeSpinner)));
}
public TimeSpan Time
{
get { return (TimeSpan)GetValue(TimeProperty); }
set { SetValue(TimeProperty, value); }
}
// Using a DependencyProperty as the backing store for Time. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register("Time", typeof(TimeSpan), typeof(TimeSpinner), new PropertyMetadata(TimeSpan.Zero));
public TimeSpan Interval
{
get { return (TimeSpan)GetValue(IntervalProperty); }
set { SetValue(IntervalProperty, value); }
}
// Using a DependencyProperty as the backing store for Interval. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IntervalProperty =
DependencyProperty.Register("Interval", typeof(TimeSpan), typeof(TimeSpinner), new PropertyMetadata(TimeSpan.FromTicks(TimeSpan.TicksPerHour)));
private static readonly TimeSpan OneDay = TimeSpan.FromTicks(TimeSpan.TicksPerDay);
void UpTime()
{
var newTime = Time + Interval;
if (newTime >= OneDay)
Time = newTime - OneDay;
else
Time = newTime;
}
void DownTime()
{
var newTime = Time - Interval;
if (newTime < TimeSpan.Zero)
Time = newTime + OneDay;
else
Time = newTime;
}
public override void OnApplyTemplate()
{
var upButton = GetUIPart<ButtonBase>("UP");
if(upButton != null)
upButton.Click += upButton_Click;
if (_upButton != null)
_upButton.Click -= upButton_Click;
_upButton = upButton;
var downButton = GetUIPart<ButtonBase>("DOWN");
if (downButton != null)
downButton.Click += downButton_Click;
if (_downButton != null)
_downButton.Click -= downButton_Click;
_downButton = downButton;
}
void downButton_Click(object sender, RoutedEventArgs e)
{
DownTime();
}
void upButton_Click(object sender, RoutedEventArgs e)
{
UpTime();
}
private ButtonBase _upButton;
private ButtonBase _downButton;
T GetUIPart<T>(string name) where T : DependencyObject
{
return (T) GetTemplateChild(name);
}
}
and a sample template (which should put in a ResourceDictionary called Generic.xaml in a folder called Themes at the root of your project):
<Style TargetType="{x:Type local:TimeSpinner}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TimeSpinner}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<UniformGrid Columns="1" DockPanel.Dock="Right">
<Button x:Name="UP" Content="+"/>
<Button x:Name="DOWN" Content="-"/>
</UniformGrid>
<TextBox Text="{Binding Time, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
</DockPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What I am trying to achieve is essentially in-place editing of a databound object inside an ItemsControl in wpf.
my ItemsControl is a horizontal WrapPanel containing multiple instances of a usercontrol (NameControl) which displays as a little pink Glyph with a person's name. It looks like this
With a popup I am able to show an editor for this "Name" (Other properties of the bound object things like Address,Gender etc.) and this works absoluttely fine. My XAML at this point would be along the lines of
<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<Button Command="{Binding EditName}" BorderThickness="0" Background="Transparent" Panel.ZIndex="1">
<widgets:NameControl />
</Button>
<Popup IsOpen="{Binding IsEditMode}"
PlacementTarget="{Binding ElementName=button}"
Margin="0 5 0 0" Placement="Relative" AllowsTransparency="True" >
<Border Background="White" BorderBrush="DarkOrchid" BorderThickness="1,1,1,1" CornerRadius="5,5,5,5"
Panel.ZIndex="100">
<Grid ShowGridLines="False" Margin="5" Background="White" Width="300">
<!-- Grid Content - just editor fields/button etc -->
</Grid>
</Border>
</Popup>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Giving an output when I click a Name looking like
With this look im quite happy (apart from my awful choice of colours!!) except that the popup does not move with the widow (resize/minimize/maximize) and that popup is above everything even other windows.
So one way to solve part of that is to "attach" or lock the popup position to the element. I have not found a good/easy/xaml way to do that. Ive come across a few code-based solutions but im not sure I like that. It just has a bit of a smell about it.
Another solution ive tried to achieve is to ditch the popup and try to emulate the behaviour of a layer/panel that sits above the other names but is position over (or below, im not fussy) the associated name control.
Ive tried a few different things, mainly around setting Panel.ZIndex on controls within a PanelControl (The Grid, the WrapPanel, a DockPanel on the very top of my MainWindow) with little success. I have implemented a simple BoolToVisibilityConverter to bind my editor Grid's Visibility property to my IsEditMode view model property and that works fine, but I cant for the life of me arrange my elements in the ItemsControl to show the editor grid over the names.
To do what is described above I simply commented out the Popup and added the following binding to the Border which contains the editor grid Visibility="{Binding IsEditMode, Converter={StaticResource boolToVisibility}}".
All that does is this:
It just shows the popup under the name but not over the others.
Any help? What am I doing wrong?
Sounds like a job for the AdornerLayer to me.
My implementation will just display one 'popup' at a time, and you can hide it by clicking the button another time. But you could also add a small close button to the ContactAdorner, or stick with your OK button, or fill the AdornerLayer behind the ContactAdorner with an element that IsHitTestVisible and reacts on click by hiding the open Adorner (so clicking anywhere outside closes the popup).
Edit: Added the small close button at your request. Changes in ContactAdorner and the ContactDetailsTemplate.
Another thing that you might want to add is repositioning of the adorner once it is clipped from the bottom (I only check for clipping from the right).
Xaml:
<UserControl x:Class="WpfApplication1.ItemsControlAdorner"
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"
mc:Ignorable="d"
xmlns:local="clr-namespace:WpfApplication1"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
<!-- Template for the Adorner -->
<DataTemplate x:Key="ContactDetailsTemplate" DataType="{x:Type local:MyContact}" >
<Border Background="#BBFFFFFF" BorderBrush="DarkOrchid" BorderThickness="1" CornerRadius="5" TextElement.Foreground="DarkOrchid" >
<Grid Margin="5" Width="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Full name" />
<TextBox Grid.Row="1" Text="{Binding FullName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Text="Address" />
<TextBox Grid.Row="3" Grid.ColumnSpan="2" Text="{Binding Address}" />
<TextBlock Grid.Column="1" Text="Gender" />
<StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="1" >
<RadioButton Content="Male" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Male}}" />
<RadioButton Content="Female" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Female}}" />
</StackPanel>
<Button x:Name="PART_CloseButton" Grid.Column="2" Height="16">
<Button.Template>
<ControlTemplate>
<Border Background="#01FFFFFF" Padding="3" >
<Path Stretch="Uniform" ClipToBounds="True" Stroke="DarkOrchid" StrokeThickness="2.5" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Border>
</DataTemplate>
<!-- Button/Item style -->
<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}" >
<Setter Property="Foreground" Value="White" />
<Setter Property="FontFamily" Value="Times New Roman" />
<Setter Property="Background" Value="#CC99E6" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="MinHeight" Value="24" />
<Setter Property="Margin" Value="3,2" />
<Setter Property="Padding" Value="3,2" />
<Setter Property="Border.CornerRadius" Value="8" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="{TemplateBinding Border.CornerRadius}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" Margin="{TemplateBinding Margin}" >
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ItemsControl style -->
<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Button x:Name="button" Style="{StaticResource ButtonStyle1}" Content="{Binding FullName}" >
<i:Interaction.Behaviors>
<local:ShowAdornerBehavior DataTemplate="{StaticResource ContactDetailsTemplate}" />
</i:Interaction.Behaviors>
</Button>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding MyContacts}" Style="{StaticResource NamesStyle}" />
</Grid>
</UserControl>
ShowAdornerBehavior, ContactAdorner, EnumToBooleanConverter:
using System.Windows;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Data;
using System;
namespace WpfApplication1
{
public class ShowAdornerBehavior : Behavior<Button>
{
public DataTemplate DataTemplate { get; set; }
protected override void OnAttached()
{
this.AssociatedObject.Click += AssociatedObject_Click;
base.OnAttached();
}
void AssociatedObject_Click(object sender, RoutedEventArgs e)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(this.AssociatedObject);
var contactAdorner = new ContactAdorner(this.AssociatedObject, adornerLayer, this.AssociatedObject.DataContext, this.DataTemplate);
}
}
public class ContactAdorner : Adorner
{
private ContentPresenter _contentPresenter;
private AdornerLayer _adornerLayer;
private static Button _btn;
private VisualCollection _visualChildren;
private double _marginRight = 5;
private double _adornerDistance = 5;
private PointCollection _points;
private static ContactAdorner _currentInstance;
public ContactAdorner(Button adornedElement, AdornerLayer adornerLayer, object data, DataTemplate dataTemplate)
: base(adornedElement)
{
if (_currentInstance != null)
_currentInstance.Hide(); // hides other adorners of the same type
if (_btn != null && _btn == adornedElement)
{
_currentInstance.Hide(); // hides the adorner of this button (toggle)
_btn = null;
}
else
{
_adornerLayer = adornerLayer;
_btn = adornedElement;
// adjust position if sizes change
_adornerLayer.SizeChanged += (s, e) => { UpdatePosition(); };
_btn.SizeChanged += (s, e) => { UpdatePosition(); };
_contentPresenter = new ContentPresenter() { Content = data, ContentTemplate = dataTemplate };
// apply template explicitly: http://stackoverflow.com/questions/5679648/why-would-this-contenttemplate-findname-throw-an-invalidoperationexception-on
_contentPresenter.ApplyTemplate();
// get close button from datatemplate
Button closeBtn = _contentPresenter.ContentTemplate.FindName("PART_CloseButton", _contentPresenter) as Button;
if (closeBtn != null)
closeBtn.Click += (s, e) => { this.Hide(); _btn = null; };
_visualChildren = new VisualCollection(this); // this is needed for user interaction with the adorner layer
_visualChildren.Add(_contentPresenter);
_adornerLayer.Add(this);
_currentInstance = this;
UpdatePosition(); // position adorner
}
}
/// <summary>
/// Positioning is a bit fiddly.
/// Also, this method is only dealing with the right clip, not yet with the bottom clip.
/// </summary>
private void UpdatePosition()
{
double marginLeft = 0;
_contentPresenter.Margin = new Thickness(marginLeft, 0, _marginRight, 0); // "reset" margin to get a good measure pass
_contentPresenter.Measure(_adornerLayer.RenderSize); // measure the contentpresenter to get a DesiredSize
var contentRect = new Rect(_contentPresenter.DesiredSize);
double right = _btn.TranslatePoint(new Point(contentRect.Width, 0), _adornerLayer).X; // this does not work with the contentpresenter, so use _adornedElement
if (right > _adornerLayer.ActualWidth) // if adorner is clipped by right window border, move it to the left
marginLeft = _adornerLayer.ActualWidth - right;
_contentPresenter.Margin = new Thickness(marginLeft, _btn.ActualHeight + _adornerDistance, _marginRight, 0); // position adorner
DrawArrow();
}
private void DrawArrow()
{
Point bottomMiddleButton = new Point(_btn.ActualWidth / 2, _btn.ActualHeight - _btn.Margin.Bottom);
Point topLeftAdorner = new Point(_btn.ActualWidth / 2 - 10, _contentPresenter.Margin.Top);
Point topRightAdorner = new Point(_btn.ActualWidth / 2 + 10, _contentPresenter.Margin.Top);
PointCollection points = new PointCollection();
points.Add(bottomMiddleButton);
points.Add(topLeftAdorner);
points.Add(topRightAdorner);
_points = points; // actual drawing executed in OnRender
}
protected override void OnRender(DrawingContext drawingContext)
{
// Drawing the arrow
StreamGeometry streamGeometry = new StreamGeometry();
using (StreamGeometryContext geometryContext = streamGeometry.Open())
{
if (_points != null && _points.Any())
{
geometryContext.BeginFigure(_points[0], true, true);
geometryContext.PolyLineTo(_points.Where(p => _points.IndexOf(p) > 0).ToList(), true, true);
}
}
// Draw the polygon visual
drawingContext.DrawGeometry(Brushes.DarkOrchid, new Pen(_btn.Background, 0.5), streamGeometry);
base.OnRender(drawingContext);
}
private void Hide()
{
_adornerLayer.Remove(this);
}
protected override Size MeasureOverride(Size constraint)
{
_contentPresenter.Measure(constraint);
return _contentPresenter.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_contentPresenter.Arrange(new Rect(finalSize));
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
protected override int VisualChildrenCount
{
get { return _visualChildren.Count; }
}
}
// http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
}
ViewModel, MyContact:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<MyContact> _myContacts = new ObservableCollection<MyContact>();
public ObservableCollection<MyContact> MyContacts { get { return _myContacts; } set { _myContacts = value; OnPropertyChanged("MyContacts"); } }
public ViewModel()
{
MyContacts = new ObservableCollection<MyContact>()
{
new MyContact() { FullName = "Sigmund Freud", Gender = Gender.Male },
new MyContact() { FullName = "Abraham Lincoln", Gender = Gender.Male },
new MyContact() { FullName = "Joan Of Arc", Gender = Gender.Female },
new MyContact() { FullName = "Bob the Khann", Gender = Gender.Male, Address = "Mongolia" },
new MyContact() { FullName = "Freddy Mercury", Gender = Gender.Male },
new MyContact() { FullName = "Giordano Bruno", Gender = Gender.Male },
new MyContact() { FullName = "Socrates", Gender = Gender.Male },
new MyContact() { FullName = "Marie Curie", Gender = Gender.Female }
};
}
}
public class MyContact : INotifyPropertyChanged
{
private string _fullName;
public string FullName { get { return _fullName; } set { _fullName = value; OnPropertyChanged("FullName"); } }
private string _address;
public string Address { get { return _address; } set { _address = value; OnPropertyChanged("Address"); } }
private Gender _gender;
public Gender Gender { get { return _gender; } set { _gender = value; OnPropertyChanged("Gender"); } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public enum Gender
{
Male,
Female
}
Personally I hate WPF's built in Popup control for exactly those reasons, and my workaround is to use a Custom Popup UserControl
Basically I'll put the Popup in a panel that allows it's children to overlap, such as a Grid or a Canvas, and position it on top of whatever content it's supposed to be on top of.
It includes DependencyProperties to specify it's parent panel and if it's open or not, and is part of the normal VisualTree so it will move around with your Window and act the same way any regular UI element would.
Typical usage would look like this:
<Grid x:Name="ParentPanel">
<ItemsControl ... />
<local:PopupPanel Content="{Binding PopupContent}"
local:PopupPanel.PopupParent="{Binding ElementName=ParentPanel}"
local:PopupPanel.IsPopupVisible="{Binding IsPopupVisible}" />
</Grid>
The code for the UserControl can be found on my blog along with a downloadable example of its use, but I'll also post a copy of it here.
The XAML for the UserControl is:
<UserControl x:Class="PopupPanelSample.PopupPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PopupPanelSample"
FocusManager.IsFocusScope="True"
>
<UserControl.Template>
<ControlTemplate TargetType="{x:Type local:PopupPanel}">
<ControlTemplate.Resources>
<!-- Converter to get Popup Positioning -->
<local:ValueDividedByParameterConverter x:Key="ValueDividedByParameterConverter" />
<!-- Popup Visibility -->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<Style x:Key="PopupPanelContentStyle" TargetType="{x:Type Grid}">
<Setter Property="Grid.Visibility" Value="{Binding Path=IsPopupVisible,
RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}},
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Style>
</ControlTemplate.Resources>
<Grid x:Name="PopupPanelContent" Style="{StaticResource PopupPanelContentStyle}">
<Grid.Resources>
<!-- Storyboard to show Content -->
<Storyboard x:Key="ShowEditPanelStoryboard" SpeedRatio="5">
<DoubleAnimation
Storyboard.TargetName="PopupPanelContent"
Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
From="0.00" To="1.00" Duration="00:00:01"
/>
<DoubleAnimation
Storyboard.TargetName="PopupPanelContent"
Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleY)"
From="0.00" To="1.00" Duration="00:00:01"
/>
</Storyboard>
</Grid.Resources>
<!-- Setting up RenderTransform for Popup Animation -->
<Grid.RenderTransform>
<ScaleTransform
CenterX="{Binding Path=PopupParent.ActualWidth, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
CenterY="{Binding Path=PopupParent.ActualHeight, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
/>
</Grid.RenderTransform>
<!-- Grayscale background & prevents mouse input -->
<Rectangle
Fill="Gray"
Opacity="{Binding Path=BackgroundOpacity, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Height}"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Width}"
/>
<!-- Popup Content -->
<ContentControl x:Name="PopupContentControl"
KeyboardNavigation.TabNavigation="Cycle"
PreviewKeyDown="PopupPanel_PreviewKeyDown"
PreviewLostKeyboardFocus="PopupPanel_LostFocus"
IsVisibleChanged="PopupPanel_IsVisibleChanged"
HorizontalAlignment="Center" VerticalAlignment="Center"
>
<ContentPresenter Content="{TemplateBinding Content}" />
</ContentControl>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
And the code-behind the UserControl looks like this:
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace PopupPanelSample
{
/// <summary>
/// Panel for handling Popups:
/// - Control with name PART_DefaultFocusControl will have default focus
/// - Can define PopupParent to determine if this popup should be hosted in a parent panel or not
/// - Can define the property EnterKeyCommand to specifify what command to run when the Enter key is pressed
/// - Can define the property EscapeKeyCommand to specify what command to run when the Escape key is pressed
/// - Can define BackgroundOpacity to specify how opaque the background will be. Value is between 0 and 1.
/// </summary>
public partial class PopupPanel : UserControl
{
#region Fields
bool _isLoading = false; // Flag to tell identify when DataContext changes
private UIElement _lastFocusControl; // Last control that had focus when popup visibility changes, but isn't closed
#endregion // Fields
#region Constructors
public PopupPanel()
{
InitializeComponent();
this.DataContextChanged += Popup_DataContextChanged;
// Register a PropertyChanged event on IsPopupVisible
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.IsPopupVisibleProperty, typeof(PopupPanel));
if (dpd != null) dpd.AddValueChanged(this, delegate { IsPopupVisible_Changed(); });
dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.ContentProperty, typeof(PopupPanel));
if (dpd != null) dpd.AddValueChanged(this, delegate { Content_Changed(); });
}
#endregion // Constructors
#region Events
#region Property Change Events
// When DataContext changes
private void Popup_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
DisableAnimationWhileLoading();
}
// When Content Property changes
private void Content_Changed()
{
DisableAnimationWhileLoading();
}
// Sets an IsLoading flag so storyboard doesn't run while loading
private void DisableAnimationWhileLoading()
{
_isLoading = true;
this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
new Action(delegate() { _isLoading = false; }));
}
// Run storyboard when IsPopupVisible property changes to true
private void IsPopupVisible_Changed()
{
bool isShown = GetIsPopupVisible(this);
if (isShown && !_isLoading)
{
FrameworkElement panel = FindChild<FrameworkElement>(this, "PopupPanelContent");
if (panel != null)
{
// Run Storyboard
Storyboard animation = (Storyboard)panel.FindResource("ShowEditPanelStoryboard");
animation.Begin();
}
}
// When hiding popup, clear the LastFocusControl
if (!isShown)
{
_lastFocusControl = null;
}
}
#endregion // Change Events
#region Popup Events
// When visibility is changed, set the default focus
void PopupPanel_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
ContentControl popupControl = FindChild<ContentControl>(this, "PopupContentControl");
this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
new Action(delegate()
{
// Verify object really is visible because sometimes it's not once we switch to Render
if (!GetIsPopupVisible(this))
{
return;
}
if (_lastFocusControl != null && _lastFocusControl.Focusable)
{
_lastFocusControl.Focus();
}
else
{
_lastFocusControl = FindChild<UIElement>(popupControl, "PART_DefaultFocusControl") as UIElement;
// If we can find the part named PART_DefaultFocusControl, set focus to it
if (_lastFocusControl != null && _lastFocusControl.Focusable)
{
_lastFocusControl.Focus();
}
else
{
_lastFocusControl = FindFirstFocusableChild(popupControl);
// If no DefaultFocusControl found, try and set focus to the first focusable element found in popup
if (_lastFocusControl != null)
{
_lastFocusControl.Focus();
}
else
{
// Just give the Popup UserControl focus so it can handle keyboard input
popupControl.Focus();
}
}
}
}
)
);
}
}
// When popup loses focus but isn't hidden, store the last element that had focus so we can put it back later
void PopupPanel_LostFocus(object sender, RoutedEventArgs e)
{
DependencyObject focusScope = FocusManager.GetFocusScope(this);
_lastFocusControl = FocusManager.GetFocusedElement(focusScope) as UIElement;
}
// Keyboard Events
private void PopupPanel_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
ICommand cmd = GetPopupEscapeKeyCommand(popup);
if (cmd != null && cmd.CanExecute(null))
{
cmd.Execute(null);
e.Handled = true;
}
else
{
// By default the Escape Key closes the popup when pressed
var expression = this.GetBindingExpression(PopupPanel.IsPopupVisibleProperty);
var dataType = expression.DataItem.GetType();
dataType.GetProperties().Single(x => x.Name == expression.ParentBinding.Path.Path)
.SetValue(expression.DataItem, false, null);
}
}
else if (e.Key == Key.Enter)
{
// Don't want to run Enter command if focus is in a TextBox with AcceptsReturn = True
if (!(e.KeyboardDevice.FocusedElement is TextBox &&
(e.KeyboardDevice.FocusedElement as TextBox).AcceptsReturn == true))
{
PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
ICommand cmd = GetPopupEnterKeyCommand(popup);
if (cmd != null && cmd.CanExecute(null))
{
cmd.Execute(null);
e.Handled = true;
}
}
}
}
#endregion // Popup Events
#endregion // Events
#region Dependency Properties
// Parent for Popup
#region PopupParent
public static readonly DependencyProperty PopupParentProperty =
DependencyProperty.Register("PopupParent", typeof(FrameworkElement),
typeof(PopupPanel), new PropertyMetadata(null, null, CoercePopupParent));
private static object CoercePopupParent(DependencyObject obj, object value)
{
// If PopupParent is null, return the Window object
return (value ?? FindAncester<Window>(obj));
}
public FrameworkElement PopupParent
{
get { return (FrameworkElement)this.GetValue(PopupParentProperty); }
set { this.SetValue(PopupParentProperty, value); }
}
// Providing Get/Set methods makes them show up in the XAML designer
public static FrameworkElement GetPopupParent(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(PopupParentProperty);
}
public static void SetPopupParent(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(PopupParentProperty, value);
}
#endregion
// Popup Visibility - If popup is shown or not
#region IsPopupVisibleProperty
public static readonly DependencyProperty IsPopupVisibleProperty =
DependencyProperty.Register("IsPopupVisible", typeof(bool),
typeof(PopupPanel), new PropertyMetadata(false, null));
public static bool GetIsPopupVisible(DependencyObject obj)
{
return (bool)obj.GetValue(IsPopupVisibleProperty);
}
public static void SetIsPopupVisible(DependencyObject obj, bool value)
{
obj.SetValue(IsPopupVisibleProperty, value);
}
#endregion // IsPopupVisibleProperty
// Transparency level for the background filler outside the popup
#region BackgroundOpacityProperty
public static readonly DependencyProperty BackgroundOpacityProperty =
DependencyProperty.Register("BackgroundOpacity", typeof(double),
typeof(PopupPanel), new PropertyMetadata(.5, null));
public static double GetBackgroundOpacity(DependencyObject obj)
{
return (double)obj.GetValue(BackgroundOpacityProperty);
}
public static void SetBackgroundOpacity(DependencyObject obj, double value)
{
obj.SetValue(BackgroundOpacityProperty, value);
}
#endregion ShowBackgroundProperty
// Command to execute when Enter key is pressed
#region PopupEnterKeyCommandProperty
public static readonly DependencyProperty PopupEnterKeyCommandProperty =
DependencyProperty.RegisterAttached("PopupEnterKeyCommand", typeof(ICommand),
typeof(PopupPanel), new PropertyMetadata(null, null));
public static ICommand GetPopupEnterKeyCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(PopupEnterKeyCommandProperty);
}
public static void SetPopupEnterKeyCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(PopupEnterKeyCommandProperty, value);
}
#endregion PopupEnterKeyCommandProperty
// Command to execute when Enter key is pressed
#region PopupEscapeKeyCommandProperty
public static readonly DependencyProperty PopupEscapeKeyCommandProperty =
DependencyProperty.RegisterAttached("PopupEscapeKeyCommand", typeof(ICommand),
typeof(PopupPanel), new PropertyMetadata(null, null));
public static ICommand GetPopupEscapeKeyCommand(DependencyObject obj)
{
return (ICommand)obj.GetValue(PopupEscapeKeyCommandProperty);
}
public static void SetPopupEscapeKeyCommand(DependencyObject obj, ICommand value)
{
obj.SetValue(PopupEscapeKeyCommandProperty, value);
}
#endregion PopupEscapeKeyCommandProperty
#endregion Dependency Properties
#region Visual Tree Helpers
public static UIElement FindFirstFocusableChild(DependencyObject parent)
{
// Confirm parent is valid.
if (parent == null) return null;
UIElement foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
UIElement child = VisualTreeHelper.GetChild(parent, i) as UIElement;
// This is returning me things like ContentControls, so for now filtering to buttons/textboxes only
if (child != null && child.Focusable && child.IsVisible)
{
foundChild = child;
break;
}
// recursively drill down the tree
foundChild = FindFirstFocusableChild(child);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
return foundChild;
}
public static T FindAncester<T>(DependencyObject current)
where T : DependencyObject
{
// Need this call to avoid returning current object if it is the same type as parent we are looking for
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
/// <summary>
/// Looks for a child control within a parent by name
/// </summary>
public static T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
else
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
#endregion
}
// Converter for Popup positioning
public class ValueDividedByParameterConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double n, d;
if (double.TryParse(value.ToString(), out n)
&& double.TryParse(parameter.ToString(), out d)
&& d != 0)
{
return n / d;
}
return 0;
} public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
I am working a very simple lookless control, and I can't seem to get one of the template bindings to work. In the control I have two Dependency Properties, the one that is a string works, and the one that is an int does not.
The csharp code looks like this:
using System;
using System.Windows;
using System.Windows.Controls;
namespace ControlDemo
{
public class TextControlLookless : Control
{
#region Title
public static readonly DependencyProperty ChartTitleProperty =
DependencyProperty.Register("ChartTitle", typeof(string), typeof(TextControlLookless),
null);
public String ChartTitle
{
get { return (string)GetValue(ChartTitleProperty); }
set
{
SetValue(ChartTitleProperty, value);
}
}
#endregion
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
null);
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
#region ctor
public TextControlLookless()
{
this.DefaultStyleKey = typeof(TextControlLookless);
}
#endregion
}
}
And the xaml for the control looks like this:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ControlDemo">
<Style TargetType="local:TextControlLookless">
<Setter Property="ChartTitle" Value="Set Title" />
<Setter Property="ChartValue" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TextControlLookless">
<Grid x:Name="Root">
<Border BorderBrush="Black" BorderThickness="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{TemplateBinding ChartTitle}" />
<TextBlock Text="{TemplateBinding ChartValue}" Grid.Row="1" />
</Grid>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
When I put this on a page, I can see the ChartTitle (either Set Title, or whatever I set it to), but the ChartValue never shows up. If I change its type to a string, it does show up, so I must be missing something.
The problem is that TemplateBinding is a far more primitive operation than Binding. Binding is an actual class and includes some helpful features including the implicit conversion of strings back and forth between other data types.
TemplateBinding is purely a markup instruction and crucially in your case does not do type conversion for you. Hence the dependency property being bound to a Text property of a TextBlock must be a string.
You have two choices:-
One choice is instead using TemplateBinding give the TextBlock a name and assign its Text in the ChartValue property changed call back:-
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
new PropertyMetadata(0, OnChartValuePropertyChanged));
private static void OnChartValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextControlLookless source = d as TextControlLookless;
source.Refresh();
}
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
private TextBlock txtChartValue { get; set; }
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
txtChartValue = GetTemplateChild("txtChartValue") as TextBlock;
Refresh();
}
private void Refresh()
{
if (txtChartValue != null)
{
txtChartValue.Text = ChartValue.ToString();
}
}
where the xaml looks like:-
<TextBlock x:Name="txtChartValue" Grid.Row="1" />
The other choice is to create a private dependency property for the value with type of string:-
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
new PropertyMetadata(0, OnChartValuePropertyChanged));
private static void OnChartValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.SetValue(ChartValueStrProperty, e.NewValue.ToString());
}
private static readonly DependencyProperty ChartValueStrProperty =
DependencyProperty.Register("ChartValueStr", typeof(string), typeof(TextControlLookless),
new PropertyMetadata("0"));
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
where the xaml looks like:-
<TextBlock Text="{TemplateBinding ChartValueStr}" Grid.Row="1" />
Note that the ChartValueStrProperty is private and I haven't bothered creating a standard .NET property to cover it. TemplateBinding actually takes the property name you assign suffixes is with "Property" then looks for a static field on the target type.
Both approaches have their strengths and weaknesses. The first approach is the more common pattern but takes a little more code and is less flexiable (the control displaying the value must be a TextBlock). The second is more flexiable and uses less code but is somewhat unorthodox.
Bounty Rewarded for any solid tutorial/learning resources regarding wiring up events with templated controls.
I Have a control template like this:
<Style TargetType="local:DatePicker">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DatePicker">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" x:Name="myDatePickerContentArea">
<StackPanel Orientation="Vertical">
<Button x:Name="myTestButton" Content="Test button" />
<telerik:RadDatePicker Style="{StaticResource VisitsReportTextBoxStyle}" Foreground="#FFFFFF" x:Name="startDate" DateTimeWatermarkContent="Start Date"/>
<telerik:RadDatePicker Style="{StaticResource VisitsReportTextBoxStyle}" x:Name="endDate" DateTimeWatermarkContent="End Date"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The C# for this template is:
public class DatePicker : Control
{
public static readonly DependencyProperty StartDateSelectedDateProperty = DependencyProperty.Register("StartDateSelectedDateProperty", typeof(DateTime), typeof(DatePicker), null);
public DateTime? StartDateSelectedDate { get; set; }
public DatePicker()
{
this.DefaultStyleKey = typeof(DatePicker);
}
public override void OnApplyTemplate()
{
RadDatePicker StartDate = this.GetTemplateChild("startDate") as RadDatePicker;
StartDate.SelectionChanged += new Telerik.Windows.Controls.SelectionChangedEventHandler(StartDate_SelectionChanged);
StartDate.SelectedDate = new DateTime(2010, 01, 01);
base.OnApplyTemplate();
}
void StartDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
RadDatePicker temp = (RadDatePicker)sender;
StartDateSelectedDate = temp.SelectedDate;
}
}
My selectionChanged Event Doesn't Fire and I'm not sure why.
Any Ideas ?
Here is an example of using best practice with the sort of control I think you are attempting to build (see notes at end for some explanations):-
[TemplatePart(Name = DatePicker.ElementStartDate, Type = typeof(RadDatePicker))]
[TemplatePart(Name = DatePicker.ElementEndDate, Type = typeof(RadDatePicker))]
public class DatePicker : Control
{
public DatePicker()
{
this.DefaultStyleKey = typeof(DatePicker);
}
#region Template Part Names
private const string ElementStartDate = "startDate";
private const string ElementEndDate = "endDate";
#endregion
#region Template Parts
private RadDatePicker _StartDate;
internal RadDatePicker StartDate
{
get { return _StartDate; }
private set
{
if (_StartDate != null)
{
_StartDate.SelectionChanged -= StartDate_SelectionChanged;
}
_StartDate = value;
if (_StartDate != null)
{
_StartDate.SelectionChanged += StartDate_SelectionChanged;
}
}
}
private RadDatePicker _EndDate;
internal RadDatePicker EndDate
{
get { return _EndDate; }
private set
{
if (_EndDate!= null)
{
_EndDate.SelectionChanged -= EndDate_SelectionChanged;
}
_EndDate= value;
if (_EndDate!= null)
{
_EndDate.SelectionChanged += EndDate_SelectionChanged;
}
}
}
#endregion
public static readonly DependencyProperty StartDateSelectedDateProperty =
DependencyProperty.Register(
"StartDateSelectedDateProperty",
typeof(DateTime?),
typeof(DatePicker),
new PropertyMetaData(new DateTime(2010, 01, 01)));
public DateTime? StartDateSelectedDate
{
get { return (DateTime?)GetValue(StartDateSelectedDateProperty); }
set { SetValue(StartDateSelectedDateProperty)}
}
public static readonly DependencyProperty EndDateSelectedDateProperty =
DependencyProperty.Register(
"EndDateSelectedDateProperty",
typeof(DateTime?),
typeof(DatePicker),
new PropertyMetaData(new DateTime(2010, 01, 01)));
public DateTime? EndDateSelectedDate
{
get { return (DateTime?)GetValue(EndDateSelectedDateProperty); }
set { SetValue(EndDateSelectedDateProperty)}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
StartDate = GetTemplateChild(ElementStartDate) as RadDatePicker;
EndDate = GetTemplateChild(ElementEndDate) as RadDatePicker;
}
void StartDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
// Do stuff with StartDate here
}
void EndDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
// Do stuff with EndDate here
}
}
The template Xaml should look like:-
<Style TargetType="local:DatePicker">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DatePicker">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" x:Name="myDatePickerContentArea">
<StackPanel Orientation="Vertical">
<Button x:Name="myTestButton" Content="Test button" />
<telerik:RadDatePicker x:Name="startDate"
Style="{StaticResource VisitsReportTextBoxStyle}"
Foreground="#FFFFFF"
DateTimeWatermarkContent="Start Date"
SelectedDate="{TemplateBinding StartDateSelectedDate}"
/>
<telerik:RadDatePicker x:Name="endDate"
Style="{StaticResource VisitsReportTextBoxStyle}"
DateTimeWatermarkContent="End Date"
SelectedDate="{TemplateBinding EndDateSelectedDate}"
/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Some Explanations
A key problem your original code had was that it hadn't implemented the dependency properties correctly. Note that properties now use GetValue and SetValue, also that the property meta data is used to assign the default rather than attempting to set in in onapplytemplate.
With the properties correctly implemented the template binding should work and in fact we're done as far as getting what appears to be your original intent, hence I've left out any actual code in the event handlers.
Create constants in the code to hold the names of key template parts that you want to interact with, this allows for name changes to be less expensive to make.
Add TemplatePart attributes to the class to indicate the key elements that the code expects to find, what their names should be and what base type they are expected to have. This allows for a designer to re-template an existing control, as long the declared template parts are present somewher the control should function correctly even if its UI is radically altered.
If you need to attach event handlers for some elements create a field to hold a reference to the element and then create a property to wrap round it. The property setter should then detach and attach the event handlers as you see in the code.
Make sure bae.OnApplyTemplate is called in the override of OnApplyTemplate then as you can see its quite straight forward to assign the above created properties.
I don't have the RadDatePicker so I can't test, my only outstanding concern is where the DateTime? is the correct type for the SelectedDate property. Certainly if it is its an improvement over the Microsoft offering which seems to have drop the ball on this typical data entry requirement.
I may only guess that the problem is that for OnApplyTemplate method Implementers should always call the base implementation before their own implementation.
The other thing is that from your code it looks like it's better to use TemplateBinding(Archive)(V4) in the template xaml
<telerik:RadDatePicker SelectedDate={TemplateBinding StartDateSelectedDate}
Style="{StaticResource VisitsReportTextBoxStyle}"
Foreground="#FFFFFF" x:Name="startDate"
DateTimeWatermarkContent="Start Date"/>
I want to add some elements to a TextBox by using a Template with the extra elements and the original TextBox inserted in the right spot. I'm trying to use the AdornedElementPlaceholder just like you would do when making a Validation.ErrorTemplate But the AdornedElement is not showing up. I have simplified the example as much as possible:
<TextBox Text="Border me with my Template">
<TextBox.Template>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border BorderBrush="Green" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
</ControlTemplate>
</TextBox.Template>
</TextBox>
The result is just a green box around the space that should be my textbox!
Unfortunately, that's just not how it works. However, it's not much more difficult than that. If you want to add a green border around a text box by changing the control template, it's going to look a bit more like this:
<ControlTemplate TargetType="TextBox">
<Border x:Name="Border"
CornerRadius="2"
Background="{TemplateBinding Background}"
BorderBrush="Green"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"/>
</Border>
</ControlTemplate>
The important part in there is the ScrollViewer named PART_ContentHost. The TextBox looks for that guy when the template is applied and uses it to host the text, caret, etc. What I think you're missing is that when you override the Template for an element, you're overriding the entire control template. It's unfortunate that you can't just change bits and pieces, but that's the truth of the matter.
Of course, if you want to maintain the original look and feel of the TextBox, such that it still looks like a Win7 TextBox for instance, you'd need to do a bit more in the ControlTemplate.
For what it's worth, it looks like the template you were trying to apply would work if you're talking about using an Adorner. It's similar to how the validation templates work in WPF, but that's a whole-nother story.
Oh, and for a much simpler way to change the border to green, you should be able to just set the BorderBrush property on the TextBox itself. Of course, I really don't know exactly what you're going for.
--
HTH
Dusty
I ended up doing a Custom Control based on a HeaderedContent Control.
It will allow the user to click or hoover over some content and then show a bubble containing some other or the same content.
Usage:
<JsCustomControls:BaloonBox LabelText="{Binding Note}" WaterMark="Click to write Note" Type="ClickToShow">
<JsCustomControls:BaloonBox.Content>
<TextBox AcceptsReturn="True" Text="{Binding Path=Note}" TextWrapping="Wrap"></TextBox>
</JsCustomControls:BaloonBox.Content>
</JsCustomControls:BaloonBox>
The code for the User control:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace JsCustomControls
{
public enum BaloonBoxTypeEnum
{
ClickToShow,
MouseOverToShow,
Manual
}
[TemplatePart(Name = BaloonBox.HeaderElement, Type = typeof(ContentPresenter))]
[TemplatePart(Name = BaloonBox.ContentElement, Type = typeof(ContentPresenter))]
[TemplatePart(Name = BaloonBox.PopUpElement, Type = typeof(Popup))]
[TemplatePart(Name=BaloonBox.LabelElement,Type=typeof(Label))]
public class BaloonBox : HeaderedContentControl
{
DispatcherTimer PopupTimer = new DispatcherTimer();
private const string HeaderElement = "PART_HeaderContentControl";
private const string ContentElement = "PART_ContenContentControl";
private const string PopUpElement = "PART_PopUp";
private const string LabelElement = "PART_HeaderLabel";
private ContentPresenter headerContentControl;
private ContentPresenter contentContentControl;
private Popup popUp;
private Label headerLabel;
static BaloonBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BaloonBox), new FrameworkPropertyMetadata(typeof(BaloonBox)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
headerContentControl = GetTemplateChild(HeaderElement) as ContentPresenter;
contentContentControl = GetTemplateChild(ContentElement) as ContentPresenter;
popUp = GetTemplateChild(PopUpElement) as Popup;
headerLabel = GetTemplateChild(LabelElement) as Label;
if (headerContentControl != null) headerContentControl.MouseDown += new MouseButtonEventHandler(headerContentControl_MouseDown);
if(headerLabel!=null)headerLabel.MouseDown+=new MouseButtonEventHandler(headerContentControl_MouseDown);
if (headerContentControl != null) headerContentControl.MouseMove += new MouseEventHandler(headerContentControl_MouseMove);
if(headerLabel!=null)headerLabel.MouseMove+=new MouseEventHandler(headerContentControl_MouseMove);
PopupTimer = new DispatcherTimer();
PopupTimer.Tick += new EventHandler(PopupTimer_Tick);
if(string.IsNullOrEmpty(LabelText))
{
if (headerLabel != null) headerLabel.Foreground = Brushes.Gray;
if (headerLabel != null) headerLabel.Content = WaterMark;
}
else
{
if (headerLabel != null) headerLabel.Foreground = Brushes.Black;
if (headerLabel != null) headerLabel.Content = LabelText;
}
}
void headerContentControl_MouseMove(object sender, MouseEventArgs e)
{
if (Type == BaloonBoxTypeEnum.MouseOverToShow)
{
if (popUp != null) popUp.IsOpen = true;
PopupTimer.Interval = new TimeSpan(0, 0, 0, 2);
PopupTimer.Start();
}
}
void headerContentControl_MouseDown(object sender, MouseButtonEventArgs e)
{
if (Type == BaloonBoxTypeEnum.ClickToShow)
{
if (popUp != null) popUp.IsOpen = true;
PopupTimer.Interval = new TimeSpan(0, 0, 0, 3);
PopupTimer.Start();
}
}
void PopupTimer_Tick(object sender, EventArgs e)
{
if (!headerContentControl.IsMouseOver && !contentContentControl.IsMouseOver && !contentContentControl.IsKeyboardFocusWithin)
{
PopupTimer.Stop();
popUp.IsOpen = false;
}
}
public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register("IsOpen",
typeof (bool),
typeof (BaloonBox),
new FrameworkPropertyMetadata
(new PropertyChangedCallback
(OnIsOpenChanged)));
public bool IsOpen
{
get { return (bool) GetValue(IsOpenProperty); }
set{SetValue(IsOpenProperty,value);}
}
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BaloonBox baloonBox = (BaloonBox)d;
baloonBox.popUp.IsOpen =(bool)e.NewValue;
}
public static readonly DependencyProperty WaterMarkProperty = DependencyProperty.Register("WaterMark",
typeof (string),
typeof (BaloonBox),
new FrameworkPropertyMetadata
(new PropertyChangedCallback
(OnWatermarkChanged)));
public string WaterMark
{
get { return (string)GetValue(WaterMarkProperty); }
set { SetValue(WaterMarkProperty,value); }
}
private static void OnWatermarkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register("LabelText",
typeof (string),
typeof (BaloonBox)
,new FrameworkPropertyMetadata(new PropertyChangedCallback(OnLabelTextChanged)));
public string LabelText
{
get { return (string) GetValue(LabelTextProperty); }
set
{
SetValue(LabelTextProperty,value);
headerLabel.Content = value;
}
}
private static void OnLabelTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BaloonBox baloonBox = (BaloonBox)d;
if (string.IsNullOrEmpty(e.NewValue.ToString()))
{
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Foreground = Brushes.Gray;
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Content = baloonBox.WaterMark;
}
else
{
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Foreground = Brushes.Black;
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Content = e.NewValue;
}
}
public static readonly DependencyProperty BaloonBoxTypeProperty = DependencyProperty.Register(
"BaloonBoxType", typeof(BaloonBoxTypeEnum), typeof(BaloonBox), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnBaloonBoxTypeChanged)));
public BaloonBoxTypeEnum Type
{
get { return (BaloonBoxTypeEnum) GetValue(BaloonBoxTypeProperty);}
set { SetValue(BaloonBoxTypeProperty,value);}
}
private static void OnBaloonBoxTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//who cares?
}
}
}
</Border>
<Popup x:Name="PART_PopUp" HorizontalOffset="-50" VerticalOffset="-5" AllowsTransparency="True" IsOpen="False" PopupAnimation="Slide" PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Header}">
<Grid Height="150" Width="250" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"></ColumnDefinition>
<ColumnDefinition Width="0.050*"></ColumnDefinition>
<ColumnDefinition Width="0.900*"></ColumnDefinition>
<ColumnDefinition Width="0.050*"></ColumnDefinition>
<ColumnDefinition Width="2"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2"></RowDefinition>
<RowDefinition Height="0.300*"></RowDefinition>
<RowDefinition Height="0.700*"></RowDefinition>
<RowDefinition Height="0.100*"></RowDefinition>
<RowDefinition Height="2"></RowDefinition>
</Grid.RowDefinitions>
<Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" Data="M79,279 L119,279 135,263 135,279 319,279 319,368.5 78.5,368.5 z" Fill="#FFFDFDB3" Stretch="Fill" Stroke="Black" UseLayoutRounding="False">
</Path>
<ScrollViewer Grid.Row="2" Grid.Column="2" Grid.RowSpan="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentPresenter x:Name="PART_ContenContentControl" Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}"/>
</ScrollViewer>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>