Workaround for display bug of TimeSpanUpDown when changing days - wpf

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>

Related

How to set DP value when the custom control use in the xaml? (when declaring)

My English skill is poor because I'm not a native English speaker.
I hope you to understand.
I created a custom window that overrides the title bar shape.
The part of the xaml code is as shown below.
<Style x:Key="MainWindow" TargetType="{x:Type Window}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" />
<Setter Property="WindowStyle" Value="None"/>
<Setter Property="AllowsTransparency" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Window}">
<Grid>
<Border x:Name="PART_TITLEBAR"
Margin="2,0,2,2"
Height="30"
DockPanel.Dock="Top"
CornerRadius="2"
Background="Transparent">
This control works well except for one problem.
The problem is that can't set the value of the DP.
The part of the cs code of the control is as shown below.
[TemplatePart(Name = "PART_TITLEBAR", Type = typeof(UIElement))]
public partial class CustomWindow : Window
{
private UIElement TitleBar { get; set; }
#region Dependency Properties for appearance.
public int TitleBarHeight
{
get { return (int)GetValue(TitleBarHeightProperty); }
set { SetValue(TitleBarHeightProperty, value); }
}
// Using a DependencyProperty as the backing store for TitleBarHeight. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleBarHeightProperty =
DependencyProperty.Register("TitleBarHeight", typeof(int), typeof(CustomWindow), new PropertyMetadata(TitleBarHeightChanged));
public static void TitleBarHeightChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
CustomWindow window = dp as CustomWindow;
Border titleBar = window.TitleBar as Border;
if (titleBar == null) return;
titleBar.Height = (int)args.NewValue;
}
public SolidColorBrush TitleTextBrush
{
get { return (SolidColorBrush)GetValue(TitleTextBrushProperty); }
set { SetValue(TitleTextBrushProperty, value); }
}
// Using a DependencyProperty as the backing store for TitleTextBrush. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleTextBrushProperty =
DependencyProperty.Register("TitleTextBrush", typeof(SolidColorBrush), typeof(CustomWindow), new PropertyMetadata(TitleTextBrushChanged));
public static void TitleTextBrushChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
CustomWindow window = dp as CustomWindow;
Border titleBar = window.TitleBar as Border;
if (titleBar == null) return;
// find the textblock control of the children of the titlebar and change the value of the foreground of the control.
}
#endregion
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AttachToVisualTree();
}
private void AttachToVisualTree()
{
AttachCloseButton();
AttachMinimizeButton();
AttachMaximizeRestoreButton();
AttachTitleBar();
AttachBorders();
}
private void AttachTitleBar()
{
if (TitleBar != null)
{
TitleBar.RemoveHandler(UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnTitlebarClick));
}
UIElement titleBar = GetChildControl<UIElement>("PART_TITLEBAR");
if (titleBar != null)
{
TitleBar = titleBar;
titleBar.AddHandler(UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnTitlebarClick));
}
}
I tried to track the problem and I have found the cause.
First, I loaded the custom control in the main project and set the value of the DP on the custom control as below.
<custom:CustomWindow TitleBarHeight="20">
<.../>
</custom:CustomWindow>
And then later, I executed the project and the sequence processed as below.
The CustomWindow is created. (constructor called)
The TitleBarHeight value of the CustomWindow is set
OnApplyTemplate() of the CustomWindow is called.
According to my confirmation, sequence 2 is the starting point of the problem.
In sequence 2, WPF trying to set the TitleBarHeight value of the CustomWindow. therefore the below code is called.
public static void TitleBarHeightChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
{
CustomWindow window = dp as CustomWindow;
Border titleBar = window.TitleBar as Border;
if (titleBar == null) return;
titleBar.Height = (int)args.NewValue;
}
But at this point, the TitleBar has not be instantiated so TitleBarHeight value is not set.
As a result, it would be moved to the routine of the below.
if (titleBar == null) return;
After then later, OnApplyTemplate() is called and TitleBar is instantiated.
Summary :
when execute < custom:CustomWindow TitleBarHeight="20"> logic, at this point the TitleBar of the CustomWindow is not instantiated so TitleBarHeight value is not set.
What I should do to solve this problem?
I hope to get your help.
Thank you for reading.
Thanks for the advice, I solved this problem.
I modified xaml code as below.
<Border x:Name="PART_TITLEBAR"
Margin="2,0,2,2"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:CustomWindow}, Path=TitleBarHeight}"
DockPanel.Dock="Top"
CornerRadius="2"
Background="Transparent">
If you have a better way of doing this, please let me know.
Thank you for advice.

WPF Canvas Zoom on mouse position

I am working on an ItemsControl with Canvas as ItemPanel. Trying to implement a zoom behavior according to the answer to an other question.
The minimum code needed for reproduction should be this.
MainWindow.xaml
<Window x:Class="WpfApp7.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp7"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="{x:Type local:DesignerSheetView}" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Style.Resources>
</Style.Resources>
<Setter Property="ItemsControl.ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas local:ZoomBehavior.IsZoomable="True" local:ZoomBehavior.ZoomFactor="0.1" local:ZoomBehavior.ModifierKey="Ctrl">
<Canvas.Background>
<VisualBrush TileMode="Tile" Viewport="-1,-1,20,20" ViewportUnits="Absolute" Viewbox="-1,-1,20,20" ViewboxUnits="Absolute">
<VisualBrush.Visual>
<Grid Width="20" Height="20">
<Ellipse Height="2" Width="2" Stroke="Black" StrokeThickness="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="-1,-1" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
</Canvas>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsControl.ItemContainerStyle">
<Setter.Value>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
<Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="True" />
<Setter Property="IsEnabled" Value="True" />
</Style>
</Window.Resources>
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<local:DesignerSheetView Background="Beige">
</local:DesignerSheetView>
</ScrollViewer>
</Grid>
The DesignerSheetView codebehind:
public class DesignerSheetView : ItemsControl
{
static DesignerSheetView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DesignerSheetView), new FrameworkPropertyMetadata(typeof(DesignerSheetView)));
}
}
And the modified ZoomBehavior
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApp7
{
public static class ZoomBehavior
{
//example from https://stackoverflow.com/questions/46424149/wpf-zoom-canvas-center-on-mouse-position
#region ZoomFactor
public static double GetZoomFactor(DependencyObject obj)
{
return (double)obj.GetValue(ZoomFactorProperty);
}
public static void SetZoomFactor(DependencyObject obj, double value)
{
obj.SetValue(ZoomFactorProperty, value);
}
// Using a DependencyProperty as the backing store for ZoomFactor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZoomFactorProperty =
DependencyProperty.RegisterAttached("ZoomFactor", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.05));
#endregion
#region ModifierKey
public static ModifierKeys? GetModifierKey(DependencyObject obj)
{
return (ModifierKeys?)obj.GetValue(ModifierKeyProperty);
}
public static void SetModifierKey(DependencyObject obj, ModifierKeys? value)
{
obj.SetValue(ModifierKeyProperty, value);
}
// Using a DependencyProperty as the backing store for ModifierKey. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ModifierKeyProperty =
DependencyProperty.RegisterAttached("ModifierKey", typeof(ModifierKeys?), typeof(ZoomBehavior), new PropertyMetadata(null));
#endregion
public static TransformMode ModeOfTransform { get; set; } = TransformMode.Layout;
private static Transform _transform;
private static Canvas _view;
#region IsZoomable
public static bool GetIsZoomable(DependencyObject obj)
{
return (bool)obj.GetValue(IsZoomableProperty);
}
public static void SetIsZoomable(DependencyObject obj, bool value)
{
obj.SetValue(IsZoomableProperty, value);
}
// Using a DependencyProperty as the backing store for IsZoomable. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsZoomableProperty =
DependencyProperty.RegisterAttached(
"IsZoomable",
typeof(bool),
typeof(ZoomBehavior),
new UIPropertyMetadata(false, OnIsZoomableChanged));
#endregion
private static void OnIsZoomableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_view = d as Canvas;
if (null == _view)
{
System.Diagnostics.Debug.Assert(false, "Wrong dependency object type");
return;
}
if ((e.NewValue is bool) == false)
{
System.Diagnostics.Debug.Assert(false, "Wrong value type assigned to dependency object");
return;
}
if (true == (bool)e.NewValue)
{
_view.MouseWheel += Canvas_MouseWheel;
if (ModeOfTransform == TransformMode.Render)
{
_transform = _view.RenderTransform = new MatrixTransform();
}
else
{
_transform = _view.LayoutTransform = new MatrixTransform();
}
}
else
{
_view.MouseWheel -= Canvas_MouseWheel;
}
}
public static double GetZoomScale(DependencyObject obj)
{
return (double)obj.GetValue(ZoomScaleProperty);
}
public static void SetZoomScale(DependencyObject obj, double value)
{
obj.SetValue(ZoomScaleProperty, value);
}
// Using a DependencyProperty as the backing store for ZoomScale. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZoomScaleProperty =
DependencyProperty.RegisterAttached("ZoomScale", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.0));
private static void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
ModifierKeys? modifierkey = GetModifierKey(sender as DependencyObject);
if (!modifierkey.HasValue)
{
return;
}
if((Keyboard.Modifiers & (modifierkey.Value)) == ModifierKeys.None)
{
return;
}
if (!(_transform is MatrixTransform transform))
{
return;
}
var pos1 = e.GetPosition(_view);
double zoomfactor = GetZoomFactor(sender as DependencyObject);
double scale = GetZoomScale(sender as DependencyObject);
//scale = (e.Delta < 0) ? (scale * zoomfactor) : (scale / zoomfactor);
scale = (e.Delta < 0) ? (scale + zoomfactor) : (scale - zoomfactor);
scale = (scale < 0.1) ? 0.1 : scale;
SetZoomScale(sender as DependencyObject, scale);
var mat = transform.Matrix;
mat.ScaleAt(scale, scale, pos1.X, pos1.Y);
//transform.Matrix = mat;
if (TransformMode.Layout == ModeOfTransform)
{
_view.LayoutTransform = new MatrixTransform(mat);
}
else
{
_view.RenderTransform = new MatrixTransform(mat);
}
e.Handled = true;
}
public enum TransformMode
{
Layout,
Render,
}
}
}
I think the ZoomBehavior should be ok, I did not change it that much. The problem is somewhere in the xaml. I observe multiple things, for which I seek a solution here:
If I use the RenderTransform mode, the zoom happens at the mouse position, as intended. The problem is, that the background does not fill the container/window.
If I use the LayoutTransform mode, the background fills the window, but the zoom does not happen on the mouse position. The transform origin is at (0,0).
The ScrollBar-s are not activated, no matter which transform mode I choose.
Most of the questions on SO start with the asker trying to solve the zoom problem with layout transformation. Almost all answers use RenderTransform instead of LayoutTrasform (e.g. this, this and this). None of the answers provide explanation, why a RenderTransform better suits the task than a LayoutTransform. Is this because with LayoutTransform one needs to change the position of the Canvas too?
What should I change in order to make the RenderTransform work (background filling whole container and ScrollBars appearing)?
Basically you have to apply a LayoutTransform to your Canvas (or a parent Grid, say), translate it by the inverse of your zoom point (taking the current zoom factor into account) and then transform it back again to it's original position (taking the new zoom into account). So this:
// zoom level. typically changes by +/- 1 (or some other constant)
// at a time and updates this.Zoom which is the actual zoom
// multiplication factor.
private double _ZoomLevel = 1;
private double ZoomLevel
{
get { return this._ZoomLevel; }
set
{
var zoomPointX = this.ViewportWidth / 2;
var zoomPointY = this.ViewportHeight / 2;
if (this.MouseOnCanvas)
{
zoomPointX = this.LastMousePos.X * this.Zoom - this.HorizontalOffset;
zoomPointY = this.LastMousePos.Y * this.Zoom - this.VerticalOffset;
}
var imageX = (this.HorizontalOffset + zoomPointX) / this.Zoom;
var imageY = (this.VerticalOffset + zoomPointY) / this.Zoom;
this._ZoomLevel = value;
this.Zoom = 0.25 * Math.Pow(Math.Sqrt(2), value);
this.HorizontalOffset = imageX * this.Zoom - zoomPointX;
this.VerticalOffset = imageY * this.Zoom - zoomPointY;
}
}
A few things to note about this code:
It assumes that the Canvas is inside a ScrollViewer, so that the user can scroll around when zoomed in. For this you need a behavior that binds to the HorizontalOffset and VerticalOffset properties.
You also need to know the width and height of the scrollviewer client area, so you'll need to modify that behavior to also provide properties for those.
You need to track the current mouse coordinate relative to the Canvas, which means intercepting MouseEnter/MouseMove/MouseLeave and maintain the MouseOnCanvas property.

Locking popup position to element, or faking a popup with layers for in-place editing in an ItemsControl

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();
}
}
}

WPF custom textbox takes two tabs to get to text?

I have a custom textbox control that displays its name when there is no text inside of it, but for some curious reason I have to hit tab twice to get from the previous element to get into the control's text field. On the first tab it highlight's the TextBox's border. I went through all of the levels of the Generic.xaml file with the properties window open and searching for 'tab' but the only one that I could find was on the TextBox itself which is properly tabstopping. How do I make my control only take one tab to get into the
Generic.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SuperTB">
<Style TargetType="{x:Type local:SuperTextB}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SuperTextB}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBox Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Text, Mode=TwoWay, UpdateSourceTrigger=LostFocus }" x:Name="PART_input">
</TextBox>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
CS:
[TemplatePart(Name="PART_input")]
public class SuperTextB : Control
{
private TextBox PART_input;
static SuperTextB()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SuperTextB), new FrameworkPropertyMetadata(typeof(SuperTextB)));
}
public SuperTextB()
{
Loaded += SuperTextBLoaded;
}
void SuperTextBLoaded(object sender, RoutedEventArgs e)
{
if (PART_input.Text == string.Empty)
{
PART_input.Background = convertName();
}
}
public override void OnApplyTemplate()
{
PART_input = GetTemplateChild("PART_input") as TextBox;
if (PART_input != null)
{
PART_input.GotFocus += PartInputGotFocus;
PART_input.LostFocus += PartInputLostFocus;
}
}
void PartInputLostFocus(object sender, RoutedEventArgs e)
{
if (PART_input.Text == string.Empty)
{
PART_input.Background = convertName();
}
}
private VisualBrush convertName()
{
char[] pieces = Name.ToCharArray();
for (int x = 0; x < pieces.Length; x++)
{
if (pieces[x].Equals('_'))
pieces[x] = ' ';
}
String toReturn = "";
foreach (char c in pieces)
toReturn += c.ToString();
VisualBrush myVis = new VisualBrush();
myVis.Stretch = Stretch.None;
TextBlock myText = new TextBlock();
myText.Text = toReturn;
myText.Foreground=Brushes.Gray;
myVis.Visual=myText;
return myVis;
}
void PartInputGotFocus(object sender, RoutedEventArgs e)
{
PART_input.Background = Brushes.White;
}
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(String), typeof(SuperTextB));
public string Text
{
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
}
It's because you have a TextBox within the template for your TextBox. That TextBox is focusable, as is your outer TextBox. Set IsFocusable to False or alter your template such that it doesn't include a TextBox within it.
simply put <Setter Property="Focusable" Value="False"/>
in between <Style TargetType="{x:Type local:SuperTextB}">
<Setter Property="Template"> will fix it.

How to add events to templated control in silverlight?

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"/>

Resources