I need a layout that stacks Items horizontal or vertical. Each Item should be separated by a Splitter, so the User can change the Size (Like the Toolbox Container in VisualStudio)
The Items come from a Model, so I need a ItemsControl where I can set the ItemsSource.
My first idea was to build a CustomControl derived from Grid. Their I used the "OnVisualChildrenChanged" Method to react when a new Item is added. Then I wantet to add new Column-/Rowdefinitions (Depending wether it schould be horizontal or vertical). But here was the first Proplem. When the Grid is set to IsItemsHost="true" I get a Error that the DefinitionsCollection is not accessable...
Heres a CodeSnipped from the custom control, where I tried to add a row.
private void SetRows()
{
this.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto };
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
SetRows();
}
EDIT:
I think I found a solution right now.
First I created a CustomControl that derives from ItemsControl.
In his template I add a Grid without inserting a Itempresenter. Now I override OnItemsChange. When a Item is added to the Collection I create a FrameworkElement and set DataContext to the added Item, than I add two Column definition to the Grid and add my Item plus an Gridsplitter to the Grid. This works well, now I only need to get the ItemTemplate from my Itemscontrol to use it in the grid.
When I solved this last issue I will add a small example.
Heres my Solution so far. If someone has a better idea, you're welcome.
Heres the CustomControl Code behind:
public class ResizableItemControl : ItemsControl
{
public ObservableCollection<FrameworkElement> _gridItems = new ObservableCollection<FrameworkElement>();
private Grid _grid;
static ResizableItemControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ResizableItemControl), new FrameworkPropertyMetadata(typeof(ResizableItemControl)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (this.Template != null)
{
_grid = this.Template.FindName("PART_Grid", this) as Grid;
}
// Add all existing items to grid
foreach (var item in Items)
{
AddItemToGrid(item);
}
}
/// <summary>
/// Called when Items in ItemsCollection changing
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (Items.Count > 0)
{
//Add Items in Grid when new Items where add
var myItem = this.Items[Items.Count - 1];
AddItemToGrid(myItem);
}
break;
}
}
/// <summary>
/// Adds Items to grid plus GridSplitter
/// </summary>
/// <param name="myItem"></param>
private void AddItemToGrid(object myItem)
{
var visualItem = this.ItemTemplate.LoadContent() as FrameworkElement;
if (visualItem != null)
{
visualItem.DataContext = myItem;
if (_grid != null)
{
if (_grid.ColumnDefinitions.Count != 0)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
_grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
var gridSplitter = CreateSplitter();
Grid.SetColumn(gridSplitter, _grid.ColumnDefinitions.Count - 2);
Grid.SetColumn(visualItem, _grid.ColumnDefinitions.Count - 1);
_grid.Children.Add(gridSplitter);
_grid.Children.Add(visualItem);
//_grid.Children.Add(myTest);
}
else
{
_grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
Grid.SetColumn(visualItem, _grid.ColumnDefinitions.Count - 1);
_grid.Children.Add(visualItem);
}
}
}
}
private static GridSplitter CreateSplitter()
{
var gridSplitter = new GridSplitter() {ResizeDirection = GridResizeDirection.Columns};
gridSplitter.Width = 5;
gridSplitter.HorizontalAlignment = HorizontalAlignment.Stretch;
gridSplitter.Background = new SolidColorBrush(Colors.Black);
return gridSplitter;
}
}
The generic Xaml:
<Style TargetType="{x:Type controls:ResizableItemControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:ResizableItemControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Hidden">
<Grid x:Name="PART_Grid"></Grid>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And how to use it:
<controls:ResizableItemControl
ItemsSource="{Binding ElementName=this,Path=Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label
Content="{Binding Name}"
ToolTip="{Binding Description}"
Background="Black"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</controls:ResizableItemControl>
You could create a Stackpanel, and for each element, a Grid inside it, with the following structure:
<StackPanel Orientation="Vertical">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Button>Hallo</Button>
<GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Button>Hallo</Button>
<GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
</Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Button>Hallo</Button>
<GridSplitter Height="5" Background="Transparent" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Grid.Row="0" ResizeDirection="Rows" ResizeBehavior="CurrentAndNext"/>
</Grid>
</StackPanel>
Note that you can only resize one column at a time, but i'm sure you can refine this a bit.
I cannot add comments to waits83's answer.
Instead of create grid splitter on OnApplyTemplate, use OnItemsSourceChanged may be better.
because with OnApplyTemplate, need to bind data context before initializeComponents() in view constructor.
Here is a panel I ended up with after a few failed attempts to implement similar scenario:
class SplitPanel : Grid
{
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(
"ItemTemplate", typeof (DataTemplate), typeof (SplitPanel), new PropertyMetadata(default(DataTemplate), OnItemTemplateChanged));
public DataTemplate ItemTemplate
{
get { return (DataTemplate) GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
foreach (var child in ((SplitPanel)d).Children.OfType<ContentPresenter>())
{
child.ContentTemplate = (DataTemplate)e.NewValue;
}
}
public static readonly DependencyProperty ItemContainerStyleProperty = DependencyProperty.Register(
"ItemContainerStyle", typeof(Style), typeof(SplitPanel), new PropertyMetadata(default(Style), OnContainerStyleChanged));
public Style ItemContainerStyle
{
get { return (Style)GetValue(ItemContainerStyleProperty); }
set { SetValue(ItemContainerStyleProperty, value); }
}
private static void OnContainerStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
foreach (var child in ((SplitPanel)d).Children.OfType<ContentPresenter>())
{
child.Style = (Style) e.NewValue;
}
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource", typeof (IEnumerable), typeof (SplitPanel), new PropertyMetadata(null ,OnItemsSourceChanged));
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = (SplitPanel)d;
grid.Children.Clear();
grid.ColumnDefinitions.Clear();
var items = e.NewValue as IEnumerable;
if (items == null) return;
var children = items.Cast<object>().Select(grid.GenerateContainer).ToArray();
for (int i = 0; ; i++)
{
var child = children[i];
var column = new ColumnDefinition
{
MinWidth = GetMinColumnWidth(child),
Width = GetColumnWidth(child),
MaxWidth = GetMaxColumnWidth(child)
};
grid.ColumnDefinitions.Add(column);
grid.Children.Add(child);
SetColumn(child, i);
if (i == children.Length - 1) break;
var splitter = new GridSplitter
{
Width = 5,
ResizeBehavior = GridResizeBehavior.CurrentAndNext,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Right,
Background = Brushes.Transparent
};
SetColumn(splitter, i);
grid.Children.Add(splitter);
}
}
public static readonly DependencyProperty ColumnWidthProperty = DependencyProperty.RegisterAttached(
"ColumnWidth", typeof (GridLength), typeof (SplitPanel), new PropertyMetadata(new GridLength(1, GridUnitType.Star), OnColumnWidthChanged));
public static void SetColumnWidth(DependencyObject element, GridLength value)
{
element.SetValue(ColumnWidthProperty, value);
}
public static GridLength GetColumnWidth(DependencyObject element)
{
return (GridLength) element.GetValue(ColumnWidthProperty);
}
private static void OnColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateColumnDefinition(d, column => column.Width = (GridLength)e.NewValue);
}
public static readonly DependencyProperty MinColumnWidthProperty = DependencyProperty.RegisterAttached(
"MinColumnWidth", typeof (double), typeof (SplitPanel), new PropertyMetadata(100d, OnMinColumnWidthChanged));
public static void SetMinColumnWidth(DependencyObject element, double value)
{
element.SetValue(MinColumnWidthProperty, value);
}
public static double GetMinColumnWidth(DependencyObject element)
{
return (double) element.GetValue(MinColumnWidthProperty);
}
private static void OnMinColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateColumnDefinition(d, column => column.MinWidth = (double)e.NewValue);
}
public static readonly DependencyProperty MaxColumnWidthProperty = DependencyProperty.RegisterAttached(
"MaxColumnWidth", typeof (double), typeof (SplitPanel), new PropertyMetadata(double.MaxValue, OnMaxColumnWidthChanged));
public static void SetMaxColumnWidth(DependencyObject element, double value)
{
element.SetValue(MaxColumnWidthProperty, value);
}
public static double GetMaxColumnWidth(DependencyObject element)
{
return (double) element.GetValue(MaxColumnWidthProperty);
}
private static void OnMaxColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UpdateColumnDefinition(d, column => column.MaxWidth = (double)e.NewValue);
}
private static void UpdateColumnDefinition(DependencyObject child, Action<ColumnDefinition> updateAction)
{
var grid = VisualTreeHelper.GetParent(child) as SplitPanel;
if (grid == null) return;
var column = GetColumn((UIElement)child);
if (column >= grid.ColumnDefinitions.Count) return;
grid.Dispatcher.BeginInvoke(new Action(() => updateAction(grid.ColumnDefinitions[column])));
}
private ContentPresenter GenerateContainer(object item)
{
return new ContentPresenter {Content = item, ContentTemplate = ItemTemplate};
}
}
Usage:
<wpfApplication1:SplitPanel ItemsSource="{Binding Items}">
<wpfApplication1:SplitPanel.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="wpfApplication1:SplitPanel.ColumnWidth" Value="{Binding Width}"/>
<Setter Property="wpfApplication1:SplitPanel.MinColumnWidth" Value="10"/>
<Setter Property="wpfApplication1:SplitPanel.MaxColumnWidth" Value="100"/>
</Style>
</wpfApplication1:SplitPanel.ItemContainerStyle>
<wpfApplication1:SplitPanel.ItemTemplate>
<DataTemplate>
<TextBlock Text="123"/>
</DataTemplate>
</wpfApplication1:SplitPanel.ItemTemplate>
</wpfApplication1:SplitPanel>
It only auto-generates columns and it does not support INotifyCollectionChanged (I do not need those in my use case). It has built-in grid splitters though, and you can specify column sizes using attached properties. I hope it helps someone. :)
Related
I am trying to achieve effect where each column has its own border, but yet can not find a perfectly working solution.
This kind of look is desired but this is implemented by putting 3 borders in 3 columned Grid, which is not flexible as Grid columns and DataGrid columns are being sized sized separately
<Window x:Class="WpfApp3.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:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
<Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>
<DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="Transparent"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
This is not trivial if you want to use the DataGrid. The "problem" is that the DataGrid uses a Grid to host the cells. The cell borders are drawn using the feature of the Grid to show grid lines. You can hide the grid lines but still you have the Grid controlling the layout.
Creating the gaps you want should not come easy. The layout system of the Grid makes it an effort to implement a solution that scales well. You can extend the SelectiveScrollingGrid (the hosting panel of the DataGrid) and add the gaps when laying out the items. Knowing about DataGrid internals, I can say it is possible, but not worth the effort.
The alternative solution would be to use a ListView with a GridView as host. The problem here is that the GridView is designed to show rows as single item. You have no chance to modify margins column based. You can only adjust the content. I have not tried to modify the ListView internal layout elements or override the layout algorithm, but in context of alternative solutions I would also rule the ListView using a GridView out - but it is possible. It's not worth the effort.
Solution: Custom View
The simplest solution I can suggest is to adjust the data structure to show data column based. This way you can use a horizontal ListBox. Each item makes a column. Each column is realized as vertical ListBox. You basically have nested ListBox elements.
You would have to take care of the row mapping in order to allow selecting cells of a common row across the vertical ListBox columns.
This can be easily achieved by adding a RowIndex property to the CellItem models.
The idea is to have the horizontal ListBox display a collection of ColumnItem models. Each column item model exposes a collection of CellItem models. The CellItem items of different columns but the same row must share the same CellItem.RowIndex.
As a bonus, this solution is very easy to style. ListBox template has almost no parts compared to the significantly more complex DataGrid or the slightly more complex GridView.
To make showcasing the concept less confusing I chose to implement the grid layout as UserControl. For the sake of simplicity the logic to initialize and host the models and source collections is implemented inside this UserControl. I don't recommend this. Instantiation and hosting of the items should be outside the control e.g., inside a view model. You should add a DependencyProperty as ItemsSource for the control as data source for the internal horizontal ListBox.
Usage Example
<Window>
<ColumnsView />
</Window>
First create the data structure to populate the view.
The structure is based on the type ColumnItem, which hosts a collection of
CellItem items where each CellItem has a CellItem.RowIndex.
The CellItem items of different columns, that logically form a row must share the same CellItem.RowIndex.
ColumnItem.cs
public class ColumnItem
{
public ColumnItem(string header, IEnumerable<CellItem> items)
{
Header = header;
this.Items = new ObservableCollection<CellItem>(items);
}
public CellItem this[int rowIndex]
=> this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));
public string Header { get; }
public ObservableCollection<CellItem> Items { get; }
}
CellItem.cs
public class CellItem : INotifyPropertyChanged
{
public CellItem(int rowIndex, object value)
{
this.RowIndex = rowIndex;
this.Value = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
public int RowIndex { get; }
private object value;
public object Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
}
Build and initialize the data structure.
In this example this is all implemented in the UserControl itself with the intend to keep the example as compact as possible.
ColumnsView.xaml.cs
public partial class ColumnsView : UserControl
{
public ColumnsView()
{
InitializeComponent();
this.DataContext = this;
InitializeSourceData();
}
public InitializeSourceData()
{
this.Columns = new ObservableCollection<ColumnItem>();
for (int columnIndex = 0; columnIndex < 3; columnIndex++)
{
var cellItems = new List<CellItem>();
int asciiChar = 65;
for (int rowIndex = 0; rowIndex < 10; rowIndex++)
{
var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
var cellItem = new CellItem(rowIndex, cellValue);
cellItems.Add(cellItem);
}
var columnHeader = $"Column {columnIndex + 1}";
var columnItem = new ColumnItem(columnHeader, cellItems);
this.Columns.Add(columnItem);
}
}
private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cellsHost = sender as Selector;
var selectedCell = cellsHost.SelectedItem as CellItem;
SelectCellsOfRow(selectedCell.RowIndex);
}
private void SelectCellsOfRow(int selectedRowIndex)
{
foreach (ColumnItem columnItem in this.Columns)
{
var cellOfRow = columnItem[selectedRowIndex];
cellOfRow.IsSelected = true;
}
}
private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e)
=> this.DragStartX = Mouse.GetPosition(this).X;
private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
{
if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
{
double currentMousePositionX = Mouse.GetPosition(this).X;
listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
this.DragStartX = currentMousePositionX;
}
}
public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
where TParent : DependencyObject
{
resultElement = null;
DependencyObject parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is TParent parent)
{
resultElement = parent;
return true;
}
return parentElement != null
? TryFindVisualParentElement(parentElement, out resultElement)
: false;
}
public ObservableCollection<ColumnItem> Columns { get; }
private double DragStartX { get; set; }
}
Create the view using a horizontal ListView that renders it's ColumnItem source collection as a list of vertical ListBox elements.
ColumnsView.xaml
<UserControl x:Class="ColumnsView">
<UserControl.Resources>
<Style x:Key="ColumnGripperStyle"
TargetType="{x:Type Thumb}">
<Setter Property="Margin"
Value="-2,8" />
<Setter Property="Width"
Value="4" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Cursor"
Value="SizeWE" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Column host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Columns}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="4"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="8">
<StackPanel>
<TextBlock Text="{Binding Header}" />
<!-- Cell host. Displays cells of a column. -->
<ListBox ItemsSource="{Binding Items}"
BorderThickness="0"
Height="150"
Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<!-- Link item container selection to CellItem.IsSelected -->
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter />
<Thumb Grid.Column="1"
Style="{StaticResource ColumnGripperStyle}"
DragStarted="ColumnGripper_DragStarted"
DragDelta="ColumnGripper_DragDelta" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</UserControl>
Notes for improvement
The ColumnsView.Columns property should be a DependencyProperty to allow to use the control as binding target.
The column gap can also be a DependencyProperty of ColumnsView.
By replacing the TextBlock that displays the column header with a Button, you can easily add sorting. Having the active column that triggers the sorting e.g. lexically, you would have to sync the other passive columns and sort them based on the CellItem.RowIndex order of the active sorted column.
Maybe choose to extend Control rather than UserControl.
You can implement the CellItem to use a generic type parameter to declare the Cellitem.Value property like CellItem<TValue>.
You can implement the ColumnItem to use a generic type parameter to declare the ColumnItem.Items property like ColumnItem<TColumn>.
Add a ColumnsView.SelectedRow property that returns a collection of all CellItem items of the current selected row
With a fair amount of elbow grease, you can get the desired look by extending the native DataGrid.
Custom header and cell templates should take care of the spacing, with the appropriate background color. The AutoGeneratingColumn behavior requires more control than could easily be achieved in XAML, so I chose to create the templates in code to be able to pass the column's PropertyName.
The observant reader will already have asked themselves: "What about the border at the end of the list?". That's right, we need to be able to distinguish the last item from all others, to be able to template its border differently.
This is done with the following contract:
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
Which the row object needs to implement for the bottom border to be drawn correctly.
This also requires some custom logic when sorting, to update the value of IsLastItem. The pic with the yellow background shows the result of sorting on ThirdNumber.
The native DataGrid provides a Sorting event out of the box, but no Sorted event. The template thingy combined with the need for a custom event, led me to subclass ColumnView from DataGrid instead of declaring it as a UserControl.
I added code-behind to MainWindow, for switching the background color, but that's just for illustration purposes (as I didn't feel like implementing the Command pattern) and has nothing to do with the custom control.
The ColumnView is configured through binding. As always, feel free to extend. The current implementation expects the columns to be auto generated. In either case, the code for generating the templates is provided.
<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>
Demo code
ColumnView
public class ColumnView : DataGrid
{
public ColumnView()
{
HeadersVisibility = DataGridHeadersVisibility.Column;
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
// Hidden props from base DataGrid
base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
base.AutoGenerateColumns = true;
base.GridLinesVisibility = DataGridGridLinesVisibility.None;
// Styling
ColumnHeaderStyle = CreateColumnHeaderStyle();
CellStyle = CreateCellStyle(this);
// Event handling
AutoGeneratingColumn += OnAutoGeneratingColumn;
Sorting += OnSorting;
Sorted += OnSorted;
}
#region Hidden props
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridLength ColumnWidth
{
get => base.ColumnWidth;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DataGridGridLinesVisibility GridLinesVisibility
{
get => base.GridLinesVisibility;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new bool AutoGenerateColumns
{
get => base.AutoGenerateColumns;
set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
}
#endregion Hidden props
#region Styling
private static Style CreateColumnHeaderStyle()
=> new Style(typeof(DataGridColumnHeader))
{
Setters =
{
new Setter(BackgroundProperty, Brushes.Transparent),
new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
}
};
private static Style CreateCellStyle(ColumnView columnView)
=> new Style(typeof(DataGridCell))
{
Setters =
{
new Setter(BorderThicknessProperty, new Thickness(0.0)),
new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
}
};
#endregion Styling
#region AutoGeneratingColumn
// https://stackoverflow.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (sender is ColumnView columnView)
{
if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
{
e.Cancel = true;
}
else
{
var column = new DataGridTemplateColumn
{
CellTemplate = CreateCustomCellTemplate(e.PropertyName),
Header = e.Column.Header,
HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
HeaderStringFormat = e.Column.HeaderStringFormat,
SortMemberPath = e.PropertyName
};
e.Column = column;
}
}
}
private static DataTemplate CreateCustomCellTemplate(string path)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(StyleProperty, new Style(typeof(Border))
{
Triggers =
{
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = false,
Setters =
{
new Setter(BackgroundProperty, Brushes.White),
}
},
new DataTrigger
{
Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
Value = true,
Setters =
{
new Setter(BackgroundProperty, SystemColors.HighlightBrush),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = false,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
}
},
new DataTrigger
{
Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
Value = true,
Setters =
{
new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
}
}
}
});
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
{
// Create the data template
var customTemplate = new DataTemplate();
// Set up the wrapping border
var border = new FrameworkElementFactory(typeof(Border));
border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
border.SetValue(BackgroundProperty, Brushes.White);
border.SetValue(BorderBrushProperty, Brushes.Black);
border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));
// Set up the TextBlock
var textBlock = new FrameworkElementFactory(typeof(TextBlock));
textBlock.SetValue(TextBlock.TextProperty, propName);
textBlock.SetValue(MarginProperty, new Thickness(5.0));
// Set the visual tree of the data template
border.AppendChild(textBlock);
customTemplate.VisualTree = border;
return customTemplate;
}
#endregion AutoGeneratingColumn
#region Sorting
#region Custom Sorted Event
// https://stackoverflow.com/questions/9571178/datagrid-is-there-no-sorted-event
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));
// Provide CLR accessors for the event
public event RoutedEventHandler Sorted
{
add => AddHandler(SortedEvent, value);
remove => RemoveHandler(SortedEvent, value);
}
// This method raises the Sorted event
private void RaiseSortedEvent()
{
var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
RaiseEvent(newEventArgs);
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
base.OnSorting(eventArgs);
RaiseSortedEvent();
}
#endregion Custom Sorted Event
private static void OnSorting(object sender, DataGridSortingEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = false;
}
}
}
private static void OnSorted(object sender, RoutedEventArgs e)
{
if (sender is DataGrid dataGrid && dataGrid.HasItems)
{
if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
{
lastItem.IsLastItem = true;
}
}
}
#endregion Sorting
}
RowItem
public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
public RowItem(int firstNumber, string secondNumber, double thirdNumber)
{
FirstNumber = firstNumber;
SecondNumber = secondNumber;
ThirdNumber = thirdNumber;
}
public int FirstNumber { get; }
public string SecondNumber { get; }
public double ThirdNumber { get; }
private bool _isLastItem;
public bool IsLastItem
{
get => _isLastItem;
set
{
_isLastItem = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
public interface ICanBeLastItem
{
bool IsLastItem { get; set; }
}
MainWindow.xaml
<Window x:Class="WpfApp.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:WpfApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Switch Background" Click="ButtonBase_OnClick" />
<local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
ItemsSource="{Binding Items}"
Background="LightSteelBlue"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
if (columnView.Background == Brushes.LightSteelBlue)
{
columnView.Background = Brushes.DarkRed;
}
else if (columnView.Background == Brushes.DarkRed)
{
columnView.Background = Brushes.Green;
}
else if (columnView.Background == Brushes.Green)
{
columnView.Background = Brushes.Blue;
}
else if (columnView.Background == Brushes.Blue)
{
columnView.Background = Brushes.Yellow;
}
else
{
columnView.Background = Brushes.LightSteelBlue;
}
}
}
MainViewModel
public class MainViewModel
{
public MainViewModel()
{
Items = InitializeItems(200);
}
private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
{
var rowItems = new ObservableCollection<RowItem>();
var random = new Random();
for (var i = 0; i < numberOfItems; i++)
{
var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
var thirdNumber = Math.Round(1000 * random.NextDouble());
var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
rowItems.Add(rowItem);
}
rowItems[numberOfItems - 1].IsLastItem = true;
return rowItems;
}
public ObservableCollection<RowItem> Items { get; }
}
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();
}
}
}
When I use the following line, implementing a templated control from a Silverlight class library;
<labelSliderControl:SliderControl x:Name="sldX" Label="X" Value="4" />
I see in the VS Disign view the Slider getting the value of 4. But when I run the app the Slider starts at 0. So when I change the attribute in XAML I can see the change, but I also want the value of 4 during runtime ofcourse. Is the depencyproperty overwriting the value in this case?
Here is the templated control from a Silverlight class library;
<Style TargetType="local:SliderControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SliderControl">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<sdk:Label x:Name="lblLabel" Content="" HorizontalContentAlignment="Center" />
<Slider x:Name="sldValue" Grid.Row="1" Orientation="Vertical" />
<TextBox x:Name="txtValue" Grid.Row="2" MaxWidth="50" MinWidth="50" Text="{Binding ElementName=sldValue, Path=Value, StringFormat=\{0:N2\}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
[TemplatePart(Name = "sldValue", Type = typeof(Slider)), TemplatePart(Name = "lblLabel", Type = typeof(Label))]
public class SliderControl : Control
{
private Slider _sliderElement;
private Slider SliderElement
{
get { return _sliderElement; }
set {
if (_sliderElement != null)
_sliderElement.ValueChanged -= new RoutedPropertyChangedEventHandler<double>(sldValue_ValueChanged);;
_sliderElement = value;
if (_sliderElement != null)
_sliderElement.ValueChanged += new RoutedPropertyChangedEventHandler<double>(sldValue_ValueChanged);
}
}
public event EventHandler<ValueChangingEventArgs> ValueChanging;
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(SliderControl), new PropertyMetadata(0.0, ValuePropertyChanged));
private static void ValuePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is SliderControl)
{
SliderControl sliderUserControl = (SliderControl)obj;
if (sliderUserControl.SliderElement != null)
{
sliderUserControl.SliderElement.Value = (double)args.NewValue;
}
}
}
public SliderControl()
{
this.DefaultStyleKey = typeof(SliderControl);
}
private void sldValue_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (ValueChanging != null)
{
ValueChanging(this, new ValueChangingEventArgs(e.OldValue, e.NewValue));
}
}
public override void OnApplyTemplate()
{
SliderElement = GetTemplateChild("sldValue") as Slider;
base.OnApplyTemplate();
}
}
I don't see a "Value" Property as declared when registering the DependencyProperty.
public double Value
{
get { return (double)base.GetValue(ValueProperty); }
set { base.SetValue(ValueProperty, value); }
}
and then bind the property to the control in the constructor:
public SliderControl(){
InitializeComponent();
this.sldValue.SetBinding(Slider.ValueProperty, new Binding()
{
Source = this,
Path = new PropertyPath("Value"),
Mode = BindingMode.TwoWay
});
}
I got it to work ! I just put the solution of gmcalab in the OnApplyTemplate.
Thanks to gmcalab.
public override void OnApplyTemplate()
{
SliderElement = GetTemplateChild("sldValue") as Slider;
if (SliderElement != null)
{
SliderElement.SetBinding(Slider.ValueProperty, new Binding()
{
Source = this,
Path = new PropertyPath("Value"),
Mode = BindingMode.TwoWay
});
}
base.OnApplyTemplate();
}
I want to add some elements to a TextBox by using a Template with the extra elements and the original TextBox inserted in the right spot. I'm trying to use the AdornedElementPlaceholder just like you would do when making a Validation.ErrorTemplate But the AdornedElement is not showing up. I have simplified the example as much as possible:
<TextBox Text="Border me with my Template">
<TextBox.Template>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border BorderBrush="Green" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
</ControlTemplate>
</TextBox.Template>
</TextBox>
The result is just a green box around the space that should be my textbox!
Unfortunately, that's just not how it works. However, it's not much more difficult than that. If you want to add a green border around a text box by changing the control template, it's going to look a bit more like this:
<ControlTemplate TargetType="TextBox">
<Border x:Name="Border"
CornerRadius="2"
Background="{TemplateBinding Background}"
BorderBrush="Green"
BorderThickness="1"
SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"/>
</Border>
</ControlTemplate>
The important part in there is the ScrollViewer named PART_ContentHost. The TextBox looks for that guy when the template is applied and uses it to host the text, caret, etc. What I think you're missing is that when you override the Template for an element, you're overriding the entire control template. It's unfortunate that you can't just change bits and pieces, but that's the truth of the matter.
Of course, if you want to maintain the original look and feel of the TextBox, such that it still looks like a Win7 TextBox for instance, you'd need to do a bit more in the ControlTemplate.
For what it's worth, it looks like the template you were trying to apply would work if you're talking about using an Adorner. It's similar to how the validation templates work in WPF, but that's a whole-nother story.
Oh, and for a much simpler way to change the border to green, you should be able to just set the BorderBrush property on the TextBox itself. Of course, I really don't know exactly what you're going for.
--
HTH
Dusty
I ended up doing a Custom Control based on a HeaderedContent Control.
It will allow the user to click or hoover over some content and then show a bubble containing some other or the same content.
Usage:
<JsCustomControls:BaloonBox LabelText="{Binding Note}" WaterMark="Click to write Note" Type="ClickToShow">
<JsCustomControls:BaloonBox.Content>
<TextBox AcceptsReturn="True" Text="{Binding Path=Note}" TextWrapping="Wrap"></TextBox>
</JsCustomControls:BaloonBox.Content>
</JsCustomControls:BaloonBox>
The code for the User control:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace JsCustomControls
{
public enum BaloonBoxTypeEnum
{
ClickToShow,
MouseOverToShow,
Manual
}
[TemplatePart(Name = BaloonBox.HeaderElement, Type = typeof(ContentPresenter))]
[TemplatePart(Name = BaloonBox.ContentElement, Type = typeof(ContentPresenter))]
[TemplatePart(Name = BaloonBox.PopUpElement, Type = typeof(Popup))]
[TemplatePart(Name=BaloonBox.LabelElement,Type=typeof(Label))]
public class BaloonBox : HeaderedContentControl
{
DispatcherTimer PopupTimer = new DispatcherTimer();
private const string HeaderElement = "PART_HeaderContentControl";
private const string ContentElement = "PART_ContenContentControl";
private const string PopUpElement = "PART_PopUp";
private const string LabelElement = "PART_HeaderLabel";
private ContentPresenter headerContentControl;
private ContentPresenter contentContentControl;
private Popup popUp;
private Label headerLabel;
static BaloonBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BaloonBox), new FrameworkPropertyMetadata(typeof(BaloonBox)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
headerContentControl = GetTemplateChild(HeaderElement) as ContentPresenter;
contentContentControl = GetTemplateChild(ContentElement) as ContentPresenter;
popUp = GetTemplateChild(PopUpElement) as Popup;
headerLabel = GetTemplateChild(LabelElement) as Label;
if (headerContentControl != null) headerContentControl.MouseDown += new MouseButtonEventHandler(headerContentControl_MouseDown);
if(headerLabel!=null)headerLabel.MouseDown+=new MouseButtonEventHandler(headerContentControl_MouseDown);
if (headerContentControl != null) headerContentControl.MouseMove += new MouseEventHandler(headerContentControl_MouseMove);
if(headerLabel!=null)headerLabel.MouseMove+=new MouseEventHandler(headerContentControl_MouseMove);
PopupTimer = new DispatcherTimer();
PopupTimer.Tick += new EventHandler(PopupTimer_Tick);
if(string.IsNullOrEmpty(LabelText))
{
if (headerLabel != null) headerLabel.Foreground = Brushes.Gray;
if (headerLabel != null) headerLabel.Content = WaterMark;
}
else
{
if (headerLabel != null) headerLabel.Foreground = Brushes.Black;
if (headerLabel != null) headerLabel.Content = LabelText;
}
}
void headerContentControl_MouseMove(object sender, MouseEventArgs e)
{
if (Type == BaloonBoxTypeEnum.MouseOverToShow)
{
if (popUp != null) popUp.IsOpen = true;
PopupTimer.Interval = new TimeSpan(0, 0, 0, 2);
PopupTimer.Start();
}
}
void headerContentControl_MouseDown(object sender, MouseButtonEventArgs e)
{
if (Type == BaloonBoxTypeEnum.ClickToShow)
{
if (popUp != null) popUp.IsOpen = true;
PopupTimer.Interval = new TimeSpan(0, 0, 0, 3);
PopupTimer.Start();
}
}
void PopupTimer_Tick(object sender, EventArgs e)
{
if (!headerContentControl.IsMouseOver && !contentContentControl.IsMouseOver && !contentContentControl.IsKeyboardFocusWithin)
{
PopupTimer.Stop();
popUp.IsOpen = false;
}
}
public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register("IsOpen",
typeof (bool),
typeof (BaloonBox),
new FrameworkPropertyMetadata
(new PropertyChangedCallback
(OnIsOpenChanged)));
public bool IsOpen
{
get { return (bool) GetValue(IsOpenProperty); }
set{SetValue(IsOpenProperty,value);}
}
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BaloonBox baloonBox = (BaloonBox)d;
baloonBox.popUp.IsOpen =(bool)e.NewValue;
}
public static readonly DependencyProperty WaterMarkProperty = DependencyProperty.Register("WaterMark",
typeof (string),
typeof (BaloonBox),
new FrameworkPropertyMetadata
(new PropertyChangedCallback
(OnWatermarkChanged)));
public string WaterMark
{
get { return (string)GetValue(WaterMarkProperty); }
set { SetValue(WaterMarkProperty,value); }
}
private static void OnWatermarkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register("LabelText",
typeof (string),
typeof (BaloonBox)
,new FrameworkPropertyMetadata(new PropertyChangedCallback(OnLabelTextChanged)));
public string LabelText
{
get { return (string) GetValue(LabelTextProperty); }
set
{
SetValue(LabelTextProperty,value);
headerLabel.Content = value;
}
}
private static void OnLabelTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BaloonBox baloonBox = (BaloonBox)d;
if (string.IsNullOrEmpty(e.NewValue.ToString()))
{
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Foreground = Brushes.Gray;
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Content = baloonBox.WaterMark;
}
else
{
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Foreground = Brushes.Black;
if (baloonBox.headerLabel != null) baloonBox.headerLabel.Content = e.NewValue;
}
}
public static readonly DependencyProperty BaloonBoxTypeProperty = DependencyProperty.Register(
"BaloonBoxType", typeof(BaloonBoxTypeEnum), typeof(BaloonBox), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnBaloonBoxTypeChanged)));
public BaloonBoxTypeEnum Type
{
get { return (BaloonBoxTypeEnum) GetValue(BaloonBoxTypeProperty);}
set { SetValue(BaloonBoxTypeProperty,value);}
}
private static void OnBaloonBoxTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//who cares?
}
}
}
</Border>
<Popup x:Name="PART_PopUp" HorizontalOffset="-50" VerticalOffset="-5" AllowsTransparency="True" IsOpen="False" PopupAnimation="Slide" PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Header}">
<Grid Height="150" Width="250" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"></ColumnDefinition>
<ColumnDefinition Width="0.050*"></ColumnDefinition>
<ColumnDefinition Width="0.900*"></ColumnDefinition>
<ColumnDefinition Width="0.050*"></ColumnDefinition>
<ColumnDefinition Width="2"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2"></RowDefinition>
<RowDefinition Height="0.300*"></RowDefinition>
<RowDefinition Height="0.700*"></RowDefinition>
<RowDefinition Height="0.100*"></RowDefinition>
<RowDefinition Height="2"></RowDefinition>
</Grid.RowDefinitions>
<Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" Data="M79,279 L119,279 135,263 135,279 319,279 319,368.5 78.5,368.5 z" Fill="#FFFDFDB3" Stretch="Fill" Stroke="Black" UseLayoutRounding="False">
</Path>
<ScrollViewer Grid.Row="2" Grid.Column="2" Grid.RowSpan="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentPresenter x:Name="PART_ContenContentControl" Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}"/>
</ScrollViewer>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I have a problem with DataContextChanged not being raised on a logical child of my custom Panel control. I've narrowed it down to this:
Starting from a wizard-generated WPF application I add:
private void Window_Loaded( object sender, RoutedEventArgs e )
{
var elt = new FrameworkElement();
this.AddLogicalChild( elt );
DataContext = 42;
Debug.Assert( (int)elt.DataContext == 42 );
}
As I understand, this works because the DataContext is an inheritable dependency property.
Now, I add event handlers for DataContextChanged both on the Window (this) and its logical child:
this.DataContextChanged +=
delegate { Debug.WriteLine( "this:DataContextChanged" ); };
elt.DataContextChanged +=
delegate { Debug.WriteLine( "elt:DataContextChanged" ); };
If I run this, only the first event handler will execute. Why is this? If instead of AddLogicalChild( elt ) I do the following:
this.Content = elt;
both handlers will execute. But this is not an option in my case - I'm adding FrameworkContentElements to my control which aren't supposed to be visual children.
What's going on here? Should I do something more besides AddLogicalChild() to make it work?
(Fortunately, there is a rather simple workaround - just bind the DataContext of the element to the DataContext of the window)
BindingOperations.SetBinding( elt, FrameworkElement.DataContextProperty,
new Binding( "DataContext" ) { Source = this } );
Thank you.
You need to override the LogicalChildren property too:
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return elt; }
}
Of course, you'll want to return any logical children defined by the base implementation, too.
I'd like to add some advice to Kent's answer if someone runs into a similar problems:
If you create a custom control with multiple content objects you should ensure:
Content objects are added to the LogicalTree via AddLogicalChild()
Create your own Enumerator class and return an instance of that in the overriden LogicalChildren property
If you don't add the content objects to the logical tree you might run into problems like Bindings with ElementNames can not be resolved (ElementName is resolved by FindName which in turn uses the LogicalTree to find the elements).
What makes it even more dangerous is that my experience is that if you miss to add the objects to the logical tree the ElementName resolving works in some scenarios and it doesn't work in other scenarios.
If you don't override LogicalChildren the DataContext is not updated like described above.
Here a short example with a simple SplitContainer:
SplitContainer:
public class SplitContainer : Control
{
static SplitContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitContainer), new FrameworkPropertyMetadata(typeof(SplitContainer)));
}
/// <summary>
/// Identifies the <see cref="Child1"/> property.
/// </summary>
public static readonly DependencyProperty Child1Property =
DependencyProperty.Register(nameof(Child1), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child1PropertyChangedCallback));
/// <summary>
/// Left Container
/// </summary>
public object Child1
{
get { return (object)GetValue(Child1Property); }
set { SetValue(Child1Property, value); }
}
/// <summary>
/// Identifies the <see cref="Child2"/> property.
/// </summary>
public static readonly DependencyProperty Child2Property =
DependencyProperty.Register(nameof(Child2), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child2PropertyChangedCallback));
/// <summary>
/// Right Container
/// </summary>
public object Child2
{
get { return (object)GetValue(Child2Property); }
set { SetValue(Child2Property, value); }
}
private static void Child1PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
private static void Child2PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
protected override IEnumerator LogicalChildren
{
get
{
return new SplitContainerLogicalChildrenEnumerator(this);
}
}
}
SplitContainerLogicalChildrenEnumerator:
internal class SplitContainerLogicalChildrenEnumerator : IEnumerator
{
private readonly SplitContainer splitContainer;
private int index = -1;
public SplitContainerLogicalChildrenEnumerator(SplitContainer splitContainer)
{
this.splitContainer = splitContainer;
}
public object Current
{
get
{
if (index == 0)
{
return splitContainer.Child1;
}
else if (index == 1)
{
return splitContainer.Child2;
}
throw new InvalidOperationException("No child for this index available");
}
}
public bool MoveNext()
{
index++;
return index < 2;
}
public void Reset()
{
index = -1;
}
}
Style (e.g. in Themes/generic.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=PresentationFramework"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SplitContainerElementNameProblem">
<Style TargetType="{x:Type local:SplitContainer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SplitContainer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Child1}" />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Child2}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Sample which demonstrates that each Binding works fine:
XAML:
<Window x:Class="SplitContainerElementNameProblem.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:SplitContainerElementNameProblem"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBox x:Name="text1" Text="abc" />
</Grid>
<local:SplitContainer Grid.Row="1">
<local:SplitContainer.Child1>
<TextBox x:Name="text2"
Text="{Binding ElementName=text1, Path=Text}" />
</local:SplitContainer.Child1>
<local:SplitContainer.Child2>
<StackPanel>
<TextBox x:Name="text3"
Text="{Binding ElementName=text2, Path=Text}" />
<TextBox x:Name="text4"
Text="{Binding MyName}" />
</StackPanel>
</local:SplitContainer.Child2>
</local:SplitContainer>
</Grid>
XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyName = "Bruno";
}
public string MyName
{
get;
set;
}
}