I'm testing some codes about custom control. I had the following styles defines in Themes folder.LayerGrid.xaml.A button with an image and text. This PanelButtonStylestyle is used in layergrid.cs.
<Style TargetType="{x:Type common:LayerGrid}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch" LastChildFill="True"
Name="PART_ParentPanel">
<DockPanel.Resources>
<Style x:Key="PanelButtonStyle" TargetType="Button">
<Setter Property="OverridesDefaultStyle" Value="True"></Setter>
<Setter Property="Focusable" Value="False"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="BorderPath" Margin="0"
BorderThickness="0" Background="{StaticResource TabItemBackgroundBrushUnselected}"
BorderBrush="{StaticResource TabItem_BorderBrush_Selected}">
<StackPanel Orientation="Horizontal">
<Image Source="/MCLF;component/Images/图像 3.png" Width="15" Height="15"
HorizontalAlignment="Center " VerticalAlignment="Center"></Image>
<TextBlock TextTrimming="CharacterEllipsis"
Text ="{TemplateBinding Name}"></TextBlock>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DockPanel.Resources>
<StackPanel Name="PART_BottomCntl" Orientation="Horizontal" DockPanel.Dock="Bottom" Background="AliceBlue"></StackPanel>
<StackPanel Name="PART_LeftCntl" Orientation="Horizontal" DockPanel.Dock="Left" Background="AliceBlue">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<StackPanel Name="PART_RightCntl" Orientation="Horizontal" DockPanel.Dock="Right" Background="AliceBlue">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<Grid Name="PART_MasterGrid" IsSharedSizeScope="True" Background="AliceBlue"></Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
private Button AddToColumnStackPanel(Layer layer)
{
var btn = new Button
{
//Background = Brushes.Transparent,
//BorderBrush = Brushes.Transparent,
//BorderThickness = new Thickness(0),
//Height = 22,
//MinWidth = 55.0,
Padding = new Thickness(10, 0, 10, 0),
//FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("PanelButtonStyle"),
};
btn.Click += (o, e) =>...
}
And the DP Layer.Name is determined in MainWindow.xaml with
<controls:Layer Level="1" Orientation="Column" Name="Symbols" ColumnLocation="Left">
<controls:Layer.Content>
<Grid>
<Grid.DataContext>
<vm:MainViewModel/>
</Grid.DataContext>
</Grid>
</controls:Layer.Content>
Now the problem is the DP Name=Symbols is not bind correctly into the button PanelButtonStyle
I read a similar post but that example set the whole target type to the DP somecustomControlWPF Custom Control: TemplateBinding to Image
Update: The DP Name is in class Layer,which is used to define the properties of each layer's location, orientation, name, content etc... The class LayerGrid serves as the class backing the custom control.
In LayerGrid.cs we have:
public class LayerGrid : ContentControl
{
...
}
public class Layer : UIElement
{
public enum LayerOrientation
{
Row,
Column
}
public enum LayerColumnLocation
{
Left,
Right
}
public static readonly DependencyProperty LevelProperty;
public static readonly DependencyProperty ContentProperty;
public static readonly DependencyProperty OrientationProperty;
public static readonly DependencyProperty NameProperty;
public static readonly DependencyProperty ColumnLocationProperty;
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
static Layer()
{
NameProperty = DependencyProperty.Register(
"Name",
typeof(string),
typeof(Layer));
...
}
}
And LayerGrid.xaml serves the purpose of laying out the general panels; Each button will be added to the corresponding stackPanel such as PART_LeftCntl by calling the function above.
<DockPanel>
<StackPanel Name="PART_BottomCntl" Orientation="Horizontal" DockPanel.Dock="Bottom" Background="AliceBlue"></StackPanel>
<StackPanel Name="PART_LeftCntl" Orientation="Horizontal" DockPanel.Dock="Left" Background="AliceBlue">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<StackPanel Name="PART_RightCntl" Orientation="Horizontal" DockPanel.Dock="Right" Background="AliceBlue">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<Grid Name="PART_MasterGrid" IsSharedSizeScope="True" Background="AliceBlue"></Grid>
</DockPanel>
And MainWindow.xaml is responsible for giving the contents inside;
Update2 Below an example of the same style is attached.Only Buttonstyle is changed.https://www.dropbox.com/sh/os34tr8zl21uj4o/AAC_segoCWzAbJMFzCKHyZnpa?dl=0
//var btn = new Button
//{
// Background = Brushes.Transparent,
// BorderBrush = Brushes.Transparent,
// BorderThickness = new Thickness(0),
// Height = 22,
// MinWidth = 65.0,
// Padding = new Thickness(10, 0, 15, 0),
// FontWeight = FontWeights.Bold,
// Style = (Style)PART_MasterGrid.FindResource("buttonStyle"),
// Content = layer.Name
//};
var btn = new Button
{
Padding = new Thickness(10, 0, 10, 0),
//FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("PanelButtonStyle")
//Content = layer.Name
};
I think you are looking for how to create the binding in code behind.
So this should work for you
Binding b = new Binding("Name");
b.Source = layer;
btn.SetBinding(Button.ContentProperty, b);
so remove Content = layer.Name & add this code before btn.Click += (o, e) =>...
give it a try and see if this is what you are looking for
EDIT
After looking at your implementation I found that the buttons are directly added to parts (StackPanel) of the layered grid template instead of layer (see below). So Relative source may not help here.
However, there are ways how you can achieve your goal in this scenario. As an easy option you can leverage Content property of Button.
So start by binding the Text property it to the template's Content property
<TextBlock TextTrimming="CharacterEllipsis" FontSize="10"
Text ="{TemplateBinding Content}"></TextBlock>
then in the code you can simply use Content = layer.Name if the Name is not supposed to change.
eg
var btn = new Button
{
Padding = new Thickness(10, 0, 10, 0),
Style = (Style)PART_MasterGrid.FindResource("PanelButtonStyle"),
Content = layer.Name
};
Or alternatively you can bind Name with Content property to reflect the changes if needed.
eg
var btn = new Button
{
Padding = new Thickness(10, 0, 10, 0),
Style = (Style)PART_MasterGrid.FindResource("PanelButtonStyle")
};
Binding b = new Binding("Name");
b.Source = layer;
btn.SetBinding(Button.ContentProperty, b);
Let me know if this helps.
Related
We have a style defined as follow:
<Style x:Key="StartButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Button ... Style="{StaticResource StartBtnStyle}">
<Button.Content>
<StackPanel>
<TextBlock x:Name="Line1" Text="..." FontSize="20" />
<TextBlock x:Name="Line2" Text="..." FontSize="8" />
</StackPanel>
</Button.Content>
</Button>
</ControlTemplate>
</Setter.Value>
</Setter>
We creates a button dynamically:
var button = new Button() {
Margin = new Thickness(3d,3d,3d,10d),
Style = FindResource("StartButtonStyle") as Style,
};
We want to find the "Line1" textblock inside the new button, and set the Text property:
var line1 = (TextBlock)button.FindName("Line1");
But it finds only "null" :( How should we find the textblock inside the button? Any advice is appreciated! Thanks in advance!
Wait until the Style has been applied - there is no TextBlock elment before this - to the Button and then find the TextBlock in the visual tree:
var button = new Button()
{
Margin = new Thickness(3d, 3d, 3d, 10d),
Style = FindResource("StartButtonStyle") as Style,
};
button.Loaded += (s, e) =>
{
TextBlock line1 = FindChild<TextBlock>(button, "Line1");
if(line1 != null)
{
line1.Text = "...";
}
};
The recursive FindChild<T> method is from here.
I copied an example about dock panel from "WPF 4.5 Unleashed" ch.5. In the example the dock is set to be in the right of the grid So when in the code behind layer0.ColumnDefinitions.Add(column1CloneForLayer0); called, a column with content column1CloneForLayer0 will be created in the right side of the grid.
In my project I almost did the same thing, but when I call the function to add a column with some content, some empty space's created at the right side.. which is not as expected.So what's the right thing to do?
1.How to Add a column in the left side of a grid?
2.In the example the column was created yet why it's empty? Maybe Do i need to set the Z Index?
Update
I added something similar in the original example in the book and get wrong result. Below is the the source codes. I just added a leftToolBox in the left.
https://www.dropbox.com/sh/61hm139j77kz9k1/AACKqhG5uXFkQgnt8fWi4NvNa?dl=0
Update2
Relevant codes: In this case I just add a stackpanel with button in the left side and and click to call DocakPane(3), instead of giving the new column in the left, it creates column in the right.
public void DockPane(int paneNumber)
{
if (paneNumber == 1)
{
pane1Button.Visibility = Visibility.Collapsed;
pane1PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column1CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 2 is docked:
if (pane2Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else if (paneNumber == 2)
{
pane2Button.Visibility = Visibility.Collapsed;
pane2PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
// Add the cloned column to layer 0:
layer0.ColumnDefinitions.Add(column2CloneForLayer0);
// Add the cloned column to layer 1, but only if pane 1 is docked:
if (pane1Button.Visibility == Visibility.Collapsed) layer1.ColumnDefinitions.Add(column2CloneForLayer1);
}
else
{
leftpane1Button.Visibility = Visibility.Collapsed;
pane3PinImage.Source = new BitmapImage(new Uri("pin.gif", UriKind.Relative));
layer0.ColumnDefinitions.Add(testcol);
}
}
Based on the sample project provided, here is a re-write of the same using MVVM and most of the problem related to hard-coding would are gone. It is not pure MVVM but it is mostly MVVM to get started with.
start by defining ViewModels
a class to represent the dock pane
class DockablePaneVM : ViewModelBase
{
public DockablePaneVM(DockHostVM host)
{
Host = host;
}
private string _title;
public string Title
{
get { return _title; }
set
{
_title = value;
RaisePropertyChanged("Title");
}
}
private bool _isPinned;
public bool IsPinned
{
get { return _isPinned; }
set
{
_isPinned = value;
Host.PinModeChanged(this);
RaisePropertyChanged("IsPinned");
}
}
private object _content;
public object Content
{
get { return _content; }
set
{
_content = value;
RaisePropertyChanged("Content");
}
}
public DockHostVM Host { get; private set; }
public Dock Dock { get; set; }
}
host for the dockpanes, I have used collectionview for simplifying the location
class DockHostVM : ViewModelBase
{
public DockHostVM()
{
Panes = new ObservableCollection<DockablePaneVM>();
LeftPanes = new CollectionViewSource() { Source = Panes }.View;
RightPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingLeftPanes = new CollectionViewSource() { Source = Panes }.View;
FlotingRightPanes = new CollectionViewSource() { Source = Panes }.View;
LeftPanes.Filter = o => Filter(o, Dock.Left, true);
RightPanes.Filter = o => Filter(o, Dock.Right, true);
FlotingLeftPanes.Filter = o => Filter(o, Dock.Left, false);
FlotingRightPanes.Filter = o => Filter(o, Dock.Right, false);
}
private bool Filter(object obj, Dock dock, bool isPinned)
{
DockablePaneVM vm = obj as DockablePaneVM;
return vm.Dock == dock && vm.IsPinned == isPinned;
}
public ObservableCollection<DockablePaneVM> Panes { get; set; }
public ICollectionView LeftPanes { get; set; }
public ICollectionView RightPanes { get; set; }
public ICollectionView FlotingLeftPanes { get; set; }
public ICollectionView FlotingRightPanes { get; set; }
private object _content;
public object Content
{
get { return _content; }
set { _content = value; }
}
public void PinModeChanged(DockablePaneVM paneVM)
{
LeftPanes.Refresh();
RightPanes.Refresh();
FlotingLeftPanes.Refresh();
FlotingRightPanes.Refresh();
}
//sample generator
public static DockHostVM GetSample()
{
DockHostVM vm = new DockHostVM();
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Solution Explorer", Content = new SolutionExplorerVM(), Dock = Dock.Right });
vm.Panes.Add(new DockablePaneVM(vm) { Title = "Toolbox", Content = new ToolBoxVM(), Dock = Dock.Right });
return vm;
}
}
then styles, to give provide the view for the classes
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<DataTemplate DataType="{x:Type l:DockablePaneVM}">
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Padding="8,4"
Text="{Binding Title}"
TextTrimming="CharacterEllipsis"
Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"
Foreground="{DynamicResource {x:Static SystemColors.ActiveCaptionTextBrushKey}}" />
<ToggleButton IsChecked="{Binding IsPinned}"
Grid.Column="1">
<Image Name="pinImage"
Source="pinHorizontal.gif" />
</ToggleButton>
<ContentControl Content="{Binding Content}"
Grid.Row="1"
Grid.ColumnSpan="2" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsPinned}"
Value="True">
<Setter Property="Source"
TargetName="pinImage"
Value="pin.gif" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding Title}" />
</Style>
<Style TargetType="TabItem"
x:Key="FloterItem"
BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="90" />
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TabControl">
<Setter Property="l:TabControlHelper.IsLastItemSelected"
Value="True" />
<Style.Triggers>
<Trigger Property="HasItems"
Value="false">
<Setter Property="Visibility"
Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TabControl"
x:Key="AutoResizePane"
BasedOn="{StaticResource {x:Type TabControl}}">
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="false">
<Setter Property="Width"
Value="23" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type l:ToolBoxVM}">
<ListBox Padding="10"
Grid.Row="1">
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ComboBox</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</DataTemplate>
<DataTemplate DataType="{x:Type l:SolutionExplorerVM}">
<TreeView Grid.Row="2">
<TreeViewItem Header="My Solution" IsExpanded="True">
<TreeViewItem Header="Project #1" />
<TreeViewItem Header="Project #2" />
<TreeViewItem Header="Project #3" />
</TreeViewItem>
</TreeView>
</DataTemplate>
</ResourceDictionary>
I have also created some dummy classes to represent the toolbox and solution explorer
also a helper class to improve the usability of tab control which i have used to host the dockpanes
class TabControlHelper
{
public static bool GetIsLastItemSelected(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastItemSelectedProperty);
}
public static void SetIsLastItemSelected(DependencyObject obj, bool value)
{
obj.SetValue(IsLastItemSelectedProperty, value);
}
// Using a DependencyProperty as the backing store for IsLastItemSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsLastItemSelectedProperty =
DependencyProperty.RegisterAttached("IsLastItemSelected", typeof(bool), typeof(TabControlHelper), new PropertyMetadata(false, OnIsLastItemSelected));
private static void OnIsLastItemSelected(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TabControl tc = d as TabControl;
tc.Items.CurrentChanged += (ss, ee) =>
{
if (tc.SelectedIndex < 0 && tc.Items.Count > 0)
tc.SelectedIndex = 0;
};
}
}
this will keep an item selected any time, in this project it will be used when a dock pane is pinned/unpinned
now the main window, note that I have bounded the dock Panes to 4 tab controls, left, right, left float & right float
<Window x:Class="VisualStudioLikePanes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="500"
Width="800"
WindowStartupLocation="CenterScreen"
xmlns:l="clr-namespace:VisualStudioLikePanes">
<Window.DataContext>
<ObjectDataProvider MethodName="GetSample"
ObjectType="{x:Type l:DockHostVM}" />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="FILE" />
...
<MenuItem Header="HELP" />
</Menu>
<DockPanel LastChildFill="True"
Grid.Row="1">
<Border Width="23"
DockPanel.Dock="Left"
Visibility="{Binding Visibility, ElementName=LeftFloter}" />
<Border Width="23"
DockPanel.Dock="Right"
Visibility="{Binding Visibility, ElementName=RightFloter}" />
<TabControl ItemsSource="{Binding LeftPanes}"
DockPanel.Dock="Left" />
<TabControl ItemsSource="{Binding RightPanes}"
DockPanel.Dock="Right" />
<Grid Name="layer0">
... page content
</Grid>
</DockPanel>
<TabControl ItemsSource="{Binding FlotingLeftPanes}"
Grid.Row="1"
HorizontalAlignment="Left"
TabStripPlacement="Left"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="LeftFloter" />
<TabControl ItemsSource="{Binding FlotingRightPanes}"
Grid.Row="1"
HorizontalAlignment="Right"
TabStripPlacement="Right"
Style="{StaticResource AutoResizePane}"
ItemContainerStyle="{StaticResource FloterItem}"
x:Name="RightFloter" />
</Grid>
</Window>
result is your expected behavior with MVVM approach, adding new panel is easy as Panes.Add(new DockablePaneVM(vm) { Title = "Left Toolbox", Content = new ToolBoxVM() }); rest is handled.
Demo
download the working sample VisualStudioLikePanes.zip
I have multiple column series chart getting generated in C#. I am further trying to get the legend for this chart with the checkboxes. Such that the chart displays the column series for only legend items that are checked.
I need to do this in C# code behind and not in HTML.
I have the below existing code that creates the multiple dynamic column series -
foreach (KeyValuePair<int, string> item in list)
{
foreach (System.Data.DataRow dRow in dtTable.Rows)
{
<formation of listSource>
}
ColumnSeries ser = new ColumnSeries { Title = item.Value, IndependentValueBinding = new Binding("Key"), DependentValueBinding = new Binding("Value") };
ser.ItemsSource = null;
ser.ItemsSource = listSource;
ser.DataPointStyle = columnStyleBrown;
mcChart.Series.Add(ser);
i++;
}
}
And I further want to add something to -
ser.LegendItemStyle =
So I need to know how to create a legend style with the check boxes in c#.
There can be 2 ways of achieving this-
Either by modifying the existing legend to contain check boxes also (preferred)
Or to create a new legend altogether
Can anyone please help?
Thanks in advance!
Was able to resolve this -
xaml code -
<Grid Name="LayoutRoot">
<Grid.Resources>
<Style x:Key="CategoryLegendItem" TargetType="DVC:LegendItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DVC:LegendItem">
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Owner.Visibility, Mode=TwoWay, Converter={StaticResource BooleanToVisibilityConverter1}}" Margin="0,0,3,0" />
<Rectangle Width="8" Height="8" Fill="{Binding Background}" Stroke="{Binding BorderBrush}" StrokeThickness="1" Margin="0,0,3,0" />
<DV:Title VerticalAlignment="Center" Content="{TemplateBinding Content}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<DVC:Chart Name="mcChart" >
</DVC:Chart>
Relevant C# code for dynamic column series -
ColumnSeries ser = new ColumnSeries { Title = kvpNuclide.Value, IndependentValueBinding = new Binding("Key"), DependentValueBinding = new Binding("Value") };
ser.ItemsSource = null;
ser.ItemsSource = listRelease;
ser.DataPointStyle = columnStyleAqua;
ser.LegendItemStyle = (Style)LayoutRoot.Resources["CategoryLegendItem"];
mcChart.Series.Add(ser);
I have a ListViewItem that I am applying a Style to and I would like to put a dotted grey line as the bottom Border.
How can I do this in WPF? I can only see solid color brushes.
This worked great in our application, allowing us to use a real Border and not mess around with Rectangles:
<Border BorderThickness="1,0,1,1">
<Border.BorderBrush>
<DrawingBrush Viewport="0,0,8,8" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,50,50" />
<RectangleGeometry Rect="50,50,50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.BorderBrush>
<TextBlock Text="Content Goes Here!" Margin="5"/>
</Border>
Note that the Viewport determines the size of the dashes in the lines. In this case, it generates eight-pixel dashes. Viewport="0,0,4,4" would give you four-pixel dashes.
You can create a dotted or dashes line using a rectangle like in the code below
<Rectangle Stroke="#FF000000" Height="1" StrokeThickness="1" StrokeDashArray="4 4"
SnapsToDevicePixels="True"/>
Get started with this and customize your listview according to your scenario
A bit late to the party, but the following solution worked for me. It is slightly simpler/better than both other solutions:
<Border BorderThickness="1">
<Border.BorderBrush>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle StrokeDashArray="4 2" Stroke="Gray" StrokeThickness="1"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}"/>
</VisualBrush.Visual>
</VisualBrush>
</Border.BorderBrush>
<TextBlock Text="Whatever" />
</Border>
Xaml
<Grid>
<Grid.RowDefinitions><RowDefinition Height="auto"/></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="auto"/></Grid.ColumnDefinitions>
<Rectangle RadiusX="9" RadiusY="9" Fill="White" Stroke="Black" StrokeDashArray="1,2"/>
<TextBlock Padding = "4,2" Text="Whatever"/>
</Grid>
Our team got this as a requirement lately and we solved it by creating a custom control, DashedBorder which extends Border and adds the dashed border feature.
It has 3 new dependency properties
UseDashedBorder (bool)
DashedBorderBrush (Brush)
StrokeDashArray (DoubleCollection)
Usable like this
<controls:DashedBorder UseDashedBorder="True"
DashedBorderBrush="#878787"
StrokeDashArray="2 1"
Background="#EBEBEB"
BorderThickness="3"
CornerRadius="10 10 10 10">
<TextBlock Text="Dashed Border"
Margin="6 2 6 2"/>
</controls:DashedBorder>
And produces a result like this
When UseDashedBorder is set to true it will create a VisualBrush with 2 rectangles and set that as BorderBrush (that's why we need an extra property for the color of the actual BorderBrush). The first one is to create the dashing and the second of is to fill in the gaps with the Background of the border.
It maps the Rectangle dashing properties to the DashedBorder properties like this
StrokeDashArray => StrokeDashArray
Stroke => DashedBorderBrush
StrokeThickness => BorderThickness.Left
RadiusX => CornerRadius.TopLeft
RadiusY => CornerRadius.TopLeft
Width => ActualWidth
Height => ActualHeight
DashedBorder.cs
public class DashedBorder : Border
{
private static DoubleCollection? emptyDoubleCollection;
private static DoubleCollection EmptyDoubleCollection()
{
if (emptyDoubleCollection == null)
{
DoubleCollection doubleCollection = new DoubleCollection();
doubleCollection.Freeze();
emptyDoubleCollection = doubleCollection;
}
return emptyDoubleCollection;
}
public static readonly DependencyProperty UseDashedBorderProperty =
DependencyProperty.Register(nameof(UseDashedBorder),
typeof(bool),
typeof(DashedBorder),
new FrameworkPropertyMetadata(false, OnUseDashedBorderChanged));
public static readonly DependencyProperty DashedBorderBrushProperty =
DependencyProperty.Register(nameof(DashedBorderBrush),
typeof(Brush),
typeof(DashedBorder),
new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty StrokeDashArrayProperty =
DependencyProperty.Register(nameof(StrokeDashArray),
typeof(DoubleCollection),
typeof(DashedBorder),
new FrameworkPropertyMetadata(EmptyDoubleCollection()));
private static void OnUseDashedBorderChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
DashedBorder dashedBorder = (DashedBorder)target;
dashedBorder.UseDashedBorderChanged();
}
private Rectangle GetBoundRectangle()
{
Rectangle rectangle = new Rectangle();
rectangle.SetBinding(Rectangle.StrokeThicknessProperty, new Binding() { Source = this, Path = new PropertyPath("BorderThickness.Left") });
rectangle.SetBinding(Rectangle.RadiusXProperty, new Binding() { Source = this, Path = new PropertyPath("CornerRadius.TopLeft") });
rectangle.SetBinding(Rectangle.RadiusYProperty, new Binding() { Source = this, Path = new PropertyPath("CornerRadius.TopLeft") });
rectangle.SetBinding(Rectangle.WidthProperty, new Binding() { Source = this, Path = new PropertyPath(ActualWidthProperty) });
rectangle.SetBinding(Rectangle.HeightProperty, new Binding() { Source = this, Path = new PropertyPath(ActualHeightProperty) });
return rectangle;
}
private Rectangle GetBackgroundRectangle()
{
Rectangle rectangle = GetBoundRectangle();
rectangle.SetBinding(Rectangle.StrokeProperty, new Binding() { Source = this, Path = new PropertyPath(BackgroundProperty) });
return rectangle;
}
private Rectangle GetDashedRectangle()
{
Rectangle rectangle = GetBoundRectangle();
rectangle.SetBinding(Rectangle.StrokeDashArrayProperty, new Binding() { Source = this, Path = new PropertyPath(StrokeDashArrayProperty) });
rectangle.SetBinding(Rectangle.StrokeProperty, new Binding() { Source = this, Path = new PropertyPath(DashedBorderBrushProperty) });
Panel.SetZIndex(rectangle, 2);
return rectangle;
}
private VisualBrush CreateDashedBorderBrush()
{
VisualBrush dashedBorderBrush = new VisualBrush();
Grid grid = new Grid();
Rectangle backgroundRectangle = GetBackgroundRectangle();
Rectangle dashedRectangle = GetDashedRectangle();
grid.Children.Add(backgroundRectangle);
grid.Children.Add(dashedRectangle);
dashedBorderBrush.Visual = grid;
return dashedBorderBrush;
}
private void UseDashedBorderChanged()
{
if (UseDashedBorder)
{
BorderBrush = CreateDashedBorderBrush();
}
else
{
ClearValue(BorderBrushProperty);
}
}
public bool UseDashedBorder
{
get { return (bool)GetValue(UseDashedBorderProperty); }
set { SetValue(UseDashedBorderProperty, value); }
}
public Brush DashedBorderBrush
{
get { return (Brush)GetValue(DashedBorderBrushProperty); }
set { SetValue(DashedBorderBrushProperty, value); }
}
public DoubleCollection StrokeDashArray
{
get { return (DoubleCollection)GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
}
Working on a user control....
I have been trying a storyboard for a marching ants border. The basic grid with a rectangle and text works fine since there is no interaction. When trying to put a button inside the grid, then either the rectangle or button is visible but never both of them.
From another post:
Advanced XAML Animation effects. Pulse, Marching ants, Rotations. Alerts
Using dotNet's solution for the VisualBrush shifted the rectangle to the border with a button inside. This worked perfectly.
<UserControl.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type TextBlock}" x:Key="LOC_DG_Cell_Mid" BasedOn="{StaticResource DG_TextBlock_Mid}" >
<Setter Property="Margin" Value="5 0"/>
</Style>
<Storyboard x:Key="MarchingAnts">
<DoubleAnimation BeginTime="00:00:00"
Storyboard.TargetName="AlertBox"
Storyboard.TargetProperty="StrokeThickness"
To="4"
Duration="0:0:0.25" />
<!-- If you want to run counter-clockwise, just swap the 'From' and 'To' values. -->
<DoubleAnimation BeginTime="00:00:00" RepeatBehavior="Forever" Storyboard.TargetName="AlertBox" Storyboard.TargetProperty="StrokeDashOffset"
Duration="0:3:0" From="1000" To="0"/>
</Storyboard>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource MarchingAnts}"/>
</EventTrigger>
</UserControl.Triggers>
<Grid>
<Border BorderThickness="1">
<Border.BorderBrush>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle x:Name="AlertBox" Stroke="Red" StrokeDashOffset="2" StrokeDashArray="5" Margin="5"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}"/>
</VisualBrush.Visual>
</VisualBrush>
</Border.BorderBrush>
<Button x:Name="FinishedButton" Padding="0 5" Margin="0" Style="{StaticResource IconButton}" >
<StackPanel Orientation="Horizontal" >
<Label Style="{StaticResource ButtonLabel}" Content="Processing has Finished" />
</StackPanel>
</Button>
</Border>
</Grid>
Since Popup doesn't derive from Control and doesn't have a template, how can I define a template so that all popups look the same? I need to design one that has a certain look and don't want to have to copy markup each time one is used.
This seems pretty easy but I can't figure out how to do it. The Child property defines a logical tree but I don't see how you can pull that out into a template and reuse it.
I was looking to do the same thing and here is what I came up with:
I inherited from ContentPresenter, styled that control as I wanted and than placed the derived ContentPresenter inside my Popup, I only used 2 text blocks for the simplicity but it is easy to understand how any content could be added.
My custom control:
using System.Windows;
using System.Windows.Controls;
namespace CustomControls
{
[TemplatePart(Name = PART_PopupHeader, Type = typeof(TextBlock))]
[TemplatePart(Name = PART_PopupContent, Type = typeof(TextBlock))]
public class CustomPopupControl : ContentControl
{
private const string PART_PopupHeader = "PART_PopupHeader";
private const string PART_PopupContent = "PART_PopupContent";
private TextBlock _headerBlock = null;
private TextBlock _contentBlock = null;
static CustomPopupControl()
{
DefaultStyleKeyProperty.OverrideMetadata
(typeof(CustomPopupControl),
new FrameworkPropertyMetadata(typeof(CustomPopupControl)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_headerBlock = GetTemplateChild(PART_PopupHeader) as TextBlock;
_contentBlock = GetTemplateChild(PART_PopupContent) as TextBlock;
}
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register("HeaderText", typeof(string), typeof(CustomPopupControl), new UIPropertyMetadata(string.Empty));
public string HeaderText
{
get
{
return (string)GetValue(HeaderTextProperty);
}
set
{
SetValue(HeaderTextProperty, value);
}
}
public static readonly DependencyProperty ContentTextProperty =
DependencyProperty.Register("ContentText", typeof(string), typeof(CustomPopupControl), new UIPropertyMetadata(string.Empty));
public string ContentText
{
get
{
return (string)GetValue(ContentTextProperty);
}
set
{
SetValue(ContentTextProperty, value);
}
}
}
}
Style for the control:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Style TargetType="{x:Type local:CustomPopupControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomPopupControl}">
<Border CornerRadius="3" BorderThickness="1" BorderBrush="White">
<Border.Background>
<SolidColorBrush Color="#4b4b4b" Opacity="0.75"/>
</Border.Background>
<Border.Effect>
<DropShadowEffect ShadowDepth="0"
Color="White"
Opacity="1"
BlurRadius="5"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="{TemplateBinding HeaderText}"
Grid.Row="0"
Foreground="#5095d6"
FontWeight="Bold"
VerticalAlignment="Bottom"
Margin="{TemplateBinding Margin}"
HorizontalAlignment="Left"/>
<Rectangle Grid.Row="1" Stroke="AntiqueWhite" Margin="1 0"></Rectangle>
<TextBlock Grid.Row="2"
Grid.ColumnSpan="2"
x:Name="PART_TooltipContents"
Margin="5, 2"
Text="{TemplateBinding ContentText}"
TextWrapping="Wrap"
MaxWidth="200"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The use of the control:
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="Button1" Content="Button with popup" HorizontalAlignment="Center">
</Button>
<Button x:Name="Button2" Content="Another button with popup" HorizontalAlignment="Center">
</Button>
<Popup IsOpen="True"
FlowDirection="LeftToRight"
Margin="10"
PlacementTarget="{Binding ElementName=Button1}"
Placement="top"
StaysOpen="True">
<local2:CustomPopupControl HeaderText="Some Header Text" ContentText="Content Text that could be any text needed from a binding or other source" Margin="2">
</local2:CustomPopupControl>
</Popup>
<Popup IsOpen="True"
FlowDirection="LeftToRight"
Margin="10"
PlacementTarget="{Binding ElementName=Button2}"
Placement="Bottom"
StaysOpen="True">
<local2:CustomPopupControl HeaderText="Different header text" ContentText="Some other text" Margin="2">
</local2:CustomPopupControl>
</Popup>
</StackPanel>
I tried illustrating how some properties can be constant across all controls, others can be customized per control and others could be bound to TemplatePart, here is the final result:
Depends how you want your pop-ups to behave. If they're just for displaying information in a uniform manner, than you might want to have a class that derives from Window that has the standard formats and styling wrapped around a ContentPresenter then bind the content of the presenter to a property which can represent the custom information for each pop-up.
Then its just a matter of programatically inserting whatever custom content you want before displaying the pop-up window.
Hope it helps.