WPF Canvas Zoom on mouse position - wpf

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.

Related

WPF - Path Geometry on Canvas scaling differently than Thumb control

This is a WPF question.
I am trying to track / drag the vertices of a PathGeometry on a Canvas using Thumb Controls to drag the vertices.
It seems that the PathGeometry is scaled differently than the Thumb positions relative to the Canvas.
How can I compute the scaling ratio? Once I have that, I can use a ScaleTransform to correct it.
Thanks in advance.
Here is my XAML. I have a scale value of 3 hard coded, but it doesn't work if my window size changes:
MyControl.XAML
<UserControl x:Class="WPF_Discovery_Client.ColorOpacityControl"
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:local="clr-namespace:WPF_Discovery_Client"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style TargetType="{x:Type Thumb}" x:Key="RoundThumb">
<Style.Resources>
<Style TargetType="Border">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BorderThickness" Value="1" />
</Style>
</Style.Resources>
</Style>
</UserControl.Resources>
<Grid>
<Canvas Background="aqua" x:Name="MyCanvas" SizeChanged="MyCanvas_SizeChanged" Loaded="MyCanvas_Loaded" >
<Path Stroke="Black" StrokeThickness="1" Height="450" Stretch="Fill" Width="800" >
<Path.Fill>
<LinearGradientBrush ColorInterpolationMode="ScRgbLinearInterpolation" StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="0.17" Color="Orange"/>
<GradientStop Offset="0.34" Color="Yellow"/>
<GradientStop Offset="0.51" Color="Green"/>
<GradientStop Offset="0.68" Color="Blue"/>
<GradientStop Offset="0.85" Color="Indigo"/>
<GradientStop Offset="1.0" Color="Violet"/>
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure x:Name="MyPath" IsClosed="True" StartPoint="{Binding BottomLeftCorner, Mode=TwoWay}">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="{Binding LeftVertex, Mode=TwoWay}"/>
<LineSegment Point="{Binding MiddleVertex, Mode=TwoWay}" />
<LineSegment Point="{Binding RightVertex, Mode=TwoWay}" />
<LineSegment Point="{Binding BottomRightCorner, Mode=TwoWay}" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
<Path.RenderTransform>
<ScaleTransform ScaleX="2.0" ScaleY="2.0"/>
</Path.RenderTransform>
</Path>
<Thumb Name="LeftThumb" Style="{DynamicResource RoundThumb}" Background="White"
Width="20" Height="20" DragDelta="LeftThumb_DragDelta"
DragStarted="LeftThumb_DragStarted" DragCompleted="LeftThumb_DragCompleted"/>
<Thumb Name="MiddleThumb" Style="{DynamicResource RoundThumb}" Background="White"
Width="20" Height="20" DragDelta="MiddleThumb_DragDelta"
DragStarted="MiddleThumb_DragStarted" DragCompleted="MiddleThumb_DragCompleted"/>
<Thumb Name="RightThumb" Style="{DynamicResource RoundThumb}" Background="White"
Width="20" Height="20" DragDelta="RightThumb_DragDelta"
DragStarted="RightThumb_DragStarted" DragCompleted="RightThumb_DragCompleted"/>
</Canvas>
</Grid>
</UserControl>
And here is the code behind for the control:
MyControl.xaml.cs
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.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Globalization;
using System.Windows.Controls.Primitives;
using System.ComponentModel;
namespace WPF_Discovery_Client
{
/// <summary>
/// Interaction logic for ColorOpacityControl.xaml
/// </summary>
public partial class ColorOpacityControl : UserControl, INotifyPropertyChanged
{
const int ThumbRadius = 10;
// Bottom corners
public Point BottomRightCorner
{
get { return (Point)GetValue(BottomRightCornerProperty); }
set { SetValue(BottomRightCornerProperty, value); }
}
// Using a DependencyProperty as the backing store for BottomRightCorner. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BottomRightCornerProperty =
DependencyProperty.Register("BottomRightCorner", typeof(Point), typeof(ColorOpacityControl), new PropertyMetadata(new Point(0, 100)));
public Point BottomLeftCorner
{
get { return (Point)GetValue(BottomLeftCornerProperty); }
set { SetValue(BottomLeftCornerProperty, value); }
}
// Using a DependencyProperty as the backing store for BottomLeftCorner. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BottomLeftCornerProperty =
DependencyProperty.Register("BottomLeftCorner", typeof(Point), typeof(ColorOpacityControl), new PropertyMetadata(new Point(0, 200)));
// Thumb center locations
public Point LeftVertex
{
get { return (Point)GetValue(LeftVertexProperty); }
set { SetValue(LeftVertexProperty, value); }
}
// Using a DependencyProperty as the backing store for LeftVertex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LeftVertexProperty =
DependencyProperty.Register("LeftVertex", typeof(Point), typeof(ColorOpacityControl), new PropertyMetadata(new Point(0,266)));
public Point MiddleVertex
{
get { return (Point)GetValue(MiddleVertexProperty); }
set { SetValue(MiddleVertexProperty, value); }
}
// Using a DependencyProperty as the backing store for MiddleVertex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MiddleVertexProperty =
DependencyProperty.Register("MiddleVertex", typeof(Point), typeof(ColorOpacityControl), new PropertyMetadata(new Point(100, 100)));
public Point RightVertex
{
get { return (Point)GetValue(RightVertexProperty); }
set { SetValue(RightVertexProperty, value); }
}
// Using a DependencyProperty as the backing store for RightVertex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RightVertexProperty =
DependencyProperty.Register("RightVertex", typeof(Point), typeof(ColorOpacityControl), new PropertyMetadata(new Point(100, 50)));
public ColorOpacityControl()
{
InitializeComponent();
DataContext = this;
}
private void LeftThumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
LeftThumb.Background = Brushes.Red;
}
private void LeftThumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
LeftThumb.Background = Brushes.White;
}
private void LeftThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
//Move the Thumb to the mouse position during the drag operation
var yadjust = MyCanvas.ActualHeight + e.VerticalChange;
var xadjust = MyCanvas.ActualWidth + e.HorizontalChange;
if ((xadjust >= 0) && (yadjust >= 0))
{
// Compute new thumb location
double X = Canvas.GetLeft(LeftThumb) + e.HorizontalChange;
double Y = Canvas.GetTop(LeftThumb) + e.VerticalChange;
// Move thumb
Canvas.SetLeft(LeftThumb, X);
Canvas.SetTop(LeftThumb, Y);
// Compute center of thumb as vertex location
LeftVertex = new Point(X + LeftThumb.Width, Y + LeftThumb.Height);
NotifyPropertyChanged("LeftVertex");
}
}
private void MiddleThumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
MiddleThumb.Background = Brushes.Green;
}
private void MiddleThumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
MiddleThumb.Background = Brushes.White;
}
private void MiddleThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
//Move the Thumb to the mouse position during the drag operation
var yadjust = MyCanvas.ActualHeight + e.VerticalChange;
var xadjust = MyCanvas.ActualWidth + e.HorizontalChange;
if ((xadjust >= 0) && (yadjust >= 0))
{
// Compute new thumb location
double X = Canvas.GetLeft(MiddleThumb) + e.HorizontalChange;
double Y = Canvas.GetTop(MiddleThumb) + e.VerticalChange;
// Move thumb
Canvas.SetLeft(MiddleThumb, X);
Canvas.SetTop(MiddleThumb, Y);
// Compute center of thumb as vertex location
MiddleVertex = new Point(X + MiddleThumb.Width, Y + MiddleThumb.Height);
NotifyPropertyChanged("MiddleVertex");
}
}
private void RightThumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
RightThumb.Background = Brushes.Yellow;
}
private void RightThumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
RightThumb.Background = Brushes.White;
}
private void RightThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
//Move the Thumb to the mouse position during the drag operation
var yadjust = MyCanvas.ActualHeight + e.VerticalChange;
var xadjust = MyCanvas.ActualWidth + e.HorizontalChange;
if ((xadjust >= 0) && (yadjust >= 0))
{
// Compute new thumb location
double X = Canvas.GetLeft(RightThumb) + e.HorizontalChange;
double Y = Canvas.GetTop(RightThumb) + e.VerticalChange;
// Move thumb
Canvas.SetLeft(RightThumb, X);
Canvas.SetTop(RightThumb, Y);
// Compute center of thumb as vertex location
RightVertex = new Point(X + ThumbRadius, Y + ThumbRadius);
NotifyPropertyChanged("RightVertex");
}
}
private void MyCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Adjust bottom left corners
BottomLeftCorner = new Point(0, MyCanvas.ActualHeight);
NotifyPropertyChanged("BottomLeftCorner");
// Adjust botton right corner
BottomRightCorner = new Point(MyCanvas.ActualWidth, MyCanvas.ActualHeight);
NotifyPropertyChanged("BottomRightCorner");
}
private void InitializeVertices()
{
// Initialize bottom left corner
BottomLeftCorner = new Point(ThumbRadius, MyCanvas.ActualHeight - ThumbRadius);
NotifyPropertyChanged("BottomLeftCorner");
// Initialize bottom right corner
BottomRightCorner = new Point(MyCanvas.ActualWidth - ThumbRadius, MyCanvas.ActualHeight - ThumbRadius);
NotifyPropertyChanged("BottomRightCorner");
// Initialize right vertex
RightVertex = new Point(MyCanvas.ActualWidth - ThumbRadius, ThumbRadius);
NotifyPropertyChanged("RightVertex");
// Initialize left vertex
LeftVertex = BottomLeftCorner;
NotifyPropertyChanged("LeftVertex");
// Initialize middle vertex
MiddleVertex = new Point(MyCanvas.ActualWidth * 0.5, MyCanvas.ActualHeight * 0.5);
NotifyPropertyChanged("MiddleVertex");
// Initialize Left Thumb
Canvas.SetLeft(LeftThumb, LeftVertex.X - ThumbRadius);
Canvas.SetTop(LeftThumb, LeftVertex.Y - ThumbRadius);
// Initialize Right Thumb
Canvas.SetLeft(RightThumb, RightVertex.X - ThumbRadius);
Canvas.SetTop(RightThumb, RightVertex.Y - ThumbRadius);
// Initialize Middle Thumb
Canvas.SetLeft(MiddleThumb, MiddleVertex.X - ThumbRadius);
Canvas.SetTop(MiddleThumb, MiddleVertex.Y - ThumbRadius);
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
private void MyCanvas_Loaded(object sender, RoutedEventArgs e)
{
InitializeVertices();
}
}
}
As you can see, I am setting both the vertices and the thumbs to the same position relative to the canvas. However, if you run the code, you can see that the initial triangle with the gradient fill is much smaller than it needs to be. I want the 3 thumbs to coincide with the 2 vertices and the midpoint.
Moreover, I notice that I have a Height and Width specified for the Path, which I am not sure if I need. If I make it larger to match the size of the canvas, the triangle will grow. Should I set Height and with to *?
I am new to WPF graphics, so any help would be appreciated.
Well.. after banging my head on the wall for a couple days, I finally figured out how to fix this issue. Hopefully this will save someone a lot of time and hassle down the road.
The problem was that I had the Path width/height set to the size of the parent window, and the Stretch property set to Fill.
Once I made the following change, the scaling came out correct.
<Canvas Background="Black" x:Name="MyCanvas" SizeChanged="MyCanvas_SizeChanged" Loaded="MyCanvas_Loaded">
<Path x:Name="MyPath" Stroke="Black" StrokeThickness="1" Height="{Binding MyCanvas.ActualHeight}" Width="{Binding MyCanvas.ActualWidth}" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top">

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.

Workaround for display bug of TimeSpanUpDown when changing days

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>

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

Modifying an ItemsPanel's Grid RowDefinitionCollection

This is a followup for ItemsControl has no children during MainWindow's constructor
Based on the answer to SO question "WPF: arranging collection items in a grid", I have the following:
<ItemsControl Name="itemsControl1" ItemsSource="{Binding MyItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="theGrid" ShowGridLines="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type FrameworkElement}">
<Setter Property="Grid.Row" Value="{Binding RowIndex}" />
<Setter Property="Grid.Column" Value="{Binding ColumnIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Now, I want to set the number of rows and columns of theGrid in the code behind:
theGrid.RowDefinitions.Clear();
theGrid.ColumnDefinitions.Clear();
for (uint i = 0; i < theNumberOfRows; i++)
theGrid.RowDefinitions.Add(new RowDefinition());
for (uint i = 0; i < theNumberOfCols; i++)
theGrid.ColumnDefinitions.Add(new ColumnDefinition());
As per MattHamilton's answer there, the gird is available once itemsControl1. ItemContainerGenerator.StatusChanged fires with status of GeneratorStatus.ContainersGenerated.
However, trying to modify the grid from the event handler raises an "Cannot modify 'RowDefinitionCollection' in read-only state" exception.
So, how can I set theGrid's row and column collections before the window is shown to the user?
edit: I'm modifying the Grid's properties from itemsControl1.ItemContainerGenerator.StatusChanged event handler:
if (itemsControl1.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
itemsControl1.ItemContainerGenerator.StatusChanged -= ItemContainerGeneratorStatusChanged;
SetGridRowsAndColumns(InitialNumberOfRows, InitialMaxNumberOfCols);
Notice that SetGridRowsAndColumns(numberOfRows, numberOfCols) does work later, in a response to a button click.
I would use attached behavior as opposed to low level customisation for ItemsControl.
If all you need is a matrix control - you might consider using a row Grid instead of ItemsControl (that's what we've ended up with). ItemsControl is unlimitedly powerful creaturem but some time it can be a challange to squeeze a small but useful extra future through its sound design.
The changes you'll have to make following this approach are:
1. Use Dimension and bind it to to a Size you want it to be.
2. Create a custom ItemTemplate and add GridEx.Position to its root visual bound to the relvant Point property.
On those two, just give us a shout and I'll update my answer with more details.
here's the class:
public class GridEx
{
public static DependencyProperty DimensionProperty =
DependencyProperty.Register("Dimension",
typeof(Size),
typeof(Grid),
new PropertyMetadata((o, e) =>
{
GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
}));
public static DependencyProperty PositionProperty =
DependencyProperty.Register("Position",
typeof(Point),
typeof(UIElement),
new PropertyMetadata((o, e) =>
{
GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
}));
private static void OnDimensionChanged(Grid grid, Size resolution)
{
grid.RowDefinitions.Clear();
grid.ColumnDefinitions.Clear();
for (int i = 0; i < resolution.Width; i++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int i = 0; i < resolution.Height; i++)
{
grid.RowDefinitions.Add(new RowDefinition());
}
}
private static void OnPostionChanged(UIElement item, Point position)
{
Grid.SetColumn(item, Convert.ToInt32((position.X)));
Grid.SetRow(item, Convert.ToInt32(position.Y));
}
public static void SetDimension(Grid grid, Size dimension)
{
grid.SetValue(GridEx.DimensionProperty, dimension);
}
public static Size GetDimension(Grid grid)
{
return (Size)grid.GetValue(GridEx.DimensionProperty);
}
public static void SetPosition(UIElement item, Point position)
{
item.SetValue(GridEx.PositionProperty, position);
}
public static Point GetPosition(Grid grid)
{
return (Point)grid.GetValue(GridEx.PositionProperty);
}
}
And here's how we use it:
<Window x:Class="GridDefs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GridDefs"
Title="MainWindow" Height="350" Width="525">
<Grid local:GridEx.Dimension="3,3">
<Button local:GridEx.Position="0,0">A</Button>
<Button local:GridEx.Position="1,1">A</Button>
<Button local:GridEx.Position="2,2">A</Button>
</Grid>
</Window>
Here's how you can create a matrix without using ItemsControl, note, that you pertain the main thing - the ability to specify templates for the items.
Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace GridDefs
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//
this.DataContext = this; // alterantively use RelativeSource
}
public IEnumerable<Item> MyItems
{
get
{
List<Item> items = new List<Item>(3);
items.Add(Item.Create("A1", new Point(0, 0)));
items.Add(Item.Create("B2", new Point(1, 1)));
items.Add(Item.Create("C3", new Point(2, 2)));
return items;
}
}
}
public interface IMatrixItem
{
Point Position { get; }
}
// Model, note - it has to implement INotifyPropertyChanged if
// you want to propagate its changes up to the UI
public class Item: IMatrixItem
{
public static Item Create(string text,
Point position)
{
Item item = new Item();
item.Text = text;
item.Position = position;
return item;
}
public string Text
{
get;
private set;
}
public Point Position
{
get;
private set;
}
}
public class GridEx
{
public static DependencyProperty DimensionProperty =
DependencyProperty.RegisterAttached("Dimension",
typeof(Size),
typeof(GridEx),
new PropertyMetadata(new Size(0, 0),
(o, e) =>
{
GridEx.OnDimensionChanged((Grid)o, (Size)e.NewValue);
}));
public static DependencyProperty PositionProperty =
DependencyProperty.RegisterAttached("Position",
typeof(Point),
typeof(GridEx),
new FrameworkPropertyMetadata(new Point(-1, -1),
(o, e) =>
{
GridEx.OnPostionChanged((UIElement)o, (Point)e.NewValue);
}));
public static DependencyProperty ItemStyleProperty =
DependencyProperty.RegisterAttached("ItemStyle",
typeof(Style),
typeof(GridEx));
public static DependencyProperty ItemsProperty =
DependencyProperty.RegisterAttached("Items",
typeof(IEnumerable<IMatrixItem>),
typeof(GridEx),
new PropertyMetadata((o, e) =>
{
GridEx.OnItemsChanged((Grid)o, (IEnumerable<IMatrixItem>)e.NewValue);
}));
#region "Dimension"
private static void OnDimensionChanged(Grid grid, Size resolution)
{
grid.RowDefinitions.Clear();
grid.ColumnDefinitions.Clear();
for (int i = 0; i < resolution.Width; i++)
{
grid.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int i = 0; i < resolution.Height; i++)
{
grid.RowDefinitions.Add(new RowDefinition());
}
}
public static void SetDimension(Grid grid, Size dimension)
{
grid.SetValue(GridEx.DimensionProperty, dimension);
}
public static Size GetDimension(Grid grid)
{
return (Size)grid.GetValue(GridEx.DimensionProperty);
}
#endregion
#region "Position"
private static void OnPostionChanged(UIElement item, Point position)
{
item.SetValue(Grid.ColumnProperty, Convert.ToInt32(position.X));
item.SetValue(Grid.RowProperty, Convert.ToInt32(position.Y));
}
private static T GetParentOfType<T>(DependencyObject current)
where T : DependencyObject
{
for (DependencyObject parent = VisualTreeHelper.GetParent(current);
parent != null;
parent = VisualTreeHelper.GetParent(parent))
{
T result = parent as T;
if (result != null)
return result;
}
return null;
}
public static void SetPosition(UIElement item, Point position)
{
item.SetValue(GridEx.PositionProperty, position);
}
public static Point GetPosition(UIElement grid)
{
return (Point)grid.GetValue(GridEx.PositionProperty);
}
#endregion
#region "ItemStyle"
public static void SetItemStyle(Grid item, Style style)
{
item.SetValue(GridEx.ItemStyleProperty, style);
}
public static Style GetItemStyle(Grid grid)
{
return (Style)grid.GetValue(GridEx.ItemStyleProperty);
}
#endregion
#region "Items"
private static void OnItemsChanged(Grid grid, IEnumerable<IMatrixItem> items)
{
grid.Children.Clear();
// template
Style style = GetItemStyle(grid);
foreach (IMatrixItem item in items)
{
Control itemControl = new Control();
grid.Children.Add(itemControl);
itemControl.Style = style;
itemControl.DataContext = item;
}
}
public static void SetItems(Grid grid, IEnumerable<IMatrixItem> items)
{
grid.SetValue(GridEx.ItemsProperty, items);
}
public static IEnumerable<IMatrixItem> GetItems(Grid grid)
{
return (IEnumerable<IMatrixItem>)grid.GetValue(GridEx.ItemsProperty);
}
#endregion
}
}
Markup:
<Window x:Class="GridDefs.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GridDefs"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Control" x:Key="t">
<Setter Property="local:GridEx.Position" Value="{Binding Position}"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button Content="{Binding Text}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid local:GridEx.Dimension="3,3"
local:GridEx.ItemStyle="{StaticResource t}"
local:GridEx.Items="{Binding MyItems}">
</Grid>
</Window>

Resources