Simpel context menues in WPF
The first thin you must do is to create the context menu in the xaml page.
This is done as follows:
<UserControl.ContextMenu>
<ContextMenu Name="rightClickContextMenu" Visibility="Collapsed">
<MenuItem Header="Delete" Name="btnDelete" Click="btnDelete_Click"/>
<MenuItem Header="Open" Name="btnOpen" Click="btnOpen_Click"/>
<MenuItem Header="Copy" Name="btnCopy" Click="btnCopy_Click"/>
</ContextMenu>
</UserControl.ContextMenu>
Listen for the right click event and display the menu
private void m(object sender, MouseEventArgs e)
{
if (e.RightButton == MouseButtonState.Pressed)
{
rightClickContextMenu.PlacementRectangle = new Rect(e.GetPosition(diagram), new Size());
rightClickContextMenu.Visibility = Visibility.Visible;
}
}
Creating a WPF grid programmatically (code-behind)
Creating a grid in the XAML page is simple. Creating one with C# is not so intuitive. Here is an example of a simple 2x2 grid in XAML and then in C#.
First in XAML:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0">First Name: </TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1">John</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0">Last Name: </TextBlock>
<TextBlock Grid.Row="1" Grid.Column="1">Smith</TextBlock>
</Grid>
Now the same grid in C#:
Grid grid = new Grid();
// Set the column and row definitions
grid.ColumnDefinitions.Add(new ColumnDefinition() {
Width = new GridLength(1, GridUnitType.Auto) });
grid.ColumnDefinitions.Add(new ColumnDefinition() {
Width = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition() {
Height = new GridLength(1, GridUnitType.Auto) });
grid.RowDefinitions.Add(new RowDefinition() {
Height = new GridLength(1, GridUnitType.Auto) });
// Row 0
TextBlock tbFirstNameLabel = new TextBlock() { Text = "First Name: "};
TextBlock tbFirstName = new TextBlock() { Text = "John"};
grid.Children.Add(tbFirstNameLabel ); // Add to the grid
Grid.SetRow(tbFirstNameLabel , 0); // Specify row for previous grid addition
Grid.SetColumn(tbFirstNameLabel , 0); // Specity column for previous grid addition
grid.Children.Add(tbFirstName ); // Add to the grid
Grid.SetRow(tbFirstName , 0); // Specify row for previous grid addition
Grid.SetColumn(tbFirstName , 1); // Specity column for previous grid addition
// Row 1
TextBlock tbLastNameLabel = new TextBlock() { Text = "Last Name: "};
TextBlock tbLastName = new TextBlock() { Text = "Smith"};
grid.Children.Add(tbLastNameLabel ); // Add to the grid
Grid.SetRow(tbLastNameLabel , 1); // Specify row for previous grid addition
Grid.SetColumn(tbLastNameLabel , 0); // Specity column for previous grid addition
grid.Children.Add(tbLastName ); // Add to the grid
Grid.SetRow(tbLastName , 1); // Specify row for previous grid addition
Grid.SetColumn(tbLastName , 1); // Specity column for previous grid addition
The tricky part of the grid above is the Grid.SetRow and Gris.SetColumn specifiers.
They don't say what grid the setting is being set to. You just have to know it must follow the grid addition as you see above.
GridSplitter style – Add a grip
The grid splitter on WPF is very useful, but not easy to style. I found some code to do this. VERY useful!
Horizontal:
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1" x:Key="panelBackgroundBrush">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFE3EFFF" Offset="0" />
<GradientStop Color="#FFAFD2FF" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<Style x:Key="gridSplitterStyle" TargetType="{x:Type GridSplitter}">
<Setter Property="FrameworkElement.Height" Value="8"/>
<Setter Property="TextElement.Foreground" Value="#FF204D89" />
<Setter Property="Border.BorderBrush" Value="#FF6593CF" />
<Setter Property="Panel.Background" Value="{StaticResource panelBackgroundBrush}" />
<Setter Property="Border.BorderThickness" Value="0,1,0,0" />
<Setter Property="UIElement.SnapsToDevicePixels" Value="True" />
<Setter Property="UIElement.Focusable" Value="False" />
<Setter Property="Control.Padding" Value="7,7,7,7" />
<Setter Property="FrameworkElement.Cursor" Value="SizeNS" />
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}">
<Border BorderThickness="1,1,0,0" BorderBrush="{StaticResource panelBackgroundBrush}">
<Canvas Width="19" Height="3">
<Rectangle Fill="{StaticResource panelBackgroundBrush}" Width="2" Height="2" Canvas.Left="1" Canvas.Top="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush}" Width="2" Height="2" Canvas.Left="5" Canvas.Top="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush}" Width="2" Height="2" Canvas.Left="9" Canvas.Top="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush}" Width="2" Height="2" Canvas.Left="13" Canvas.Top="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush}" Width="2" Height="2" Canvas.Left="17" Canvas.Top="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Left="0" Canvas.Top="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Left="4" Canvas.Top="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Left="8" Canvas.Top="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Left="12" Canvas.Top="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Left="16" Canvas.Top="0" />
</Canvas>
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Vertical:
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1" x:Key="panelBackgroundBrush2">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFE3EFFF" Offset="0" />
<GradientStop Color="#FFAFD2FF" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<Style x:Key="gridSplitterVerticalStyle">
<Setter Property="FrameworkElement.Height" Value="8"/>
<Setter Property="TextElement.Foreground" Value="#FF204D89" />
<Setter Property="Border.BorderBrush" Value="#FF6593CF" />
<Setter Property="Panel.Background" Value="{StaticResource panelBackgroundBrush2}" />
<Setter Property="Border.BorderThickness" Value="0,1,0,0" />
<Setter Property="UIElement.SnapsToDevicePixels" Value="True" />
<Setter Property="UIElement.Focusable" Value="False" />
<Setter Property="Control.Padding" Value="7,7,7,7" />
<Setter Property="FrameworkElement.Cursor" Value="SizeWE" />
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}">
<Border BorderThickness="1,1,0,0" BorderBrush="{StaticResource panelBackgroundBrush2}">
<Canvas Width="3" Height="19">
<Rectangle Fill="{StaticResource panelBackgroundBrush2}" Width="2" Height="2" Canvas.Top="1" Canvas.Left="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush2}" Width="2" Height="2" Canvas.Top="5" Canvas.Left="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush2}" Width="2" Height="2" Canvas.Top="9" Canvas.Left="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush2}" Width="2" Height="2" Canvas.Top="13" Canvas.Left="0" />
<Rectangle Fill="{StaticResource panelBackgroundBrush2}" Width="2" Height="2" Canvas.Top="17" Canvas.Left="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Top="0" Canvas.Left="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Top="4" Canvas.Left="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Top="8" Canvas.Left="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Top="12" Canvas.Left="0" />
<Rectangle Fill="{TemplateBinding TextElement.Foreground}" Width="2" Height="2" Canvas.Top="16" Canvas.Left="0" />
</Canvas>
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Setting properties – in code vs. in XAML
Something you don't realize when first working with WPF is the different ways to set peroperties in code.
There are times you want to set a property for an item - like a ListBox - and you don't see the property listed in the intellisense.
Here is an exmaple of what I am talking about.
I want to turn off horizontal scrolling on a ListBox so the the content can become variable width and resize with the width of the app.
In XAML this is very easy to do:
<ListBox ScrollViewer.CanContentScroll="False" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<TextBlock TextWrapping="Wrap" HorizontalAlignment="Left">
Lots and lots of text. Lots and lots of text. Lots and lots of text. Lots and lots of text. Lots and lots of text.
</TextBlock>
</ListBox>
Here is how to do this in code:
ListBox myList = new ListBox(); // 'Items' property access in the standard way. myList.Items.Clear(); myList.Items.Add(myItem); // CanContentScroll is not a property of the ListBox. It is a property of the ScrollViewer in the ListBox's control template. // These properties can only be set this way: ScrollViewer.SetCanContentScroll(myList, false); ScrollViewer.SetHorizontalScrollBarVisibility(myList, ScrollBarVisibility.Disabled);
As you can see, this way of setting the ScrollViewer properties is very logical when you see it. But without know it is done this way, you may not think of it.
This works because the ListBox is a dependency property.
Find a child UIElement
Here is a nice little method that will find the first occurance of child UIElement.
XAML can get long and complex. This helped me find specific elements so I can set focus on them or do some other type of operation.
Call the method like this
UIElement result = FocusOnFirstElement(myLayoutRoot, typeof(Expander));
Here is the method:
/// <summary>
/// Find the first UIElement of a specific type within the specified root element.
/// UIElements in WPF have child elements under one of the following properties: Content, Child, Items, Children
/// This loops thru itself looking for any child elements.
/// </summary>
/// <param name="rootElement"></param>
/// <param name="typeToFind"></param>
/// <returns>UIElement or null if the element was not found</returns>
public UIElement FocusOnFirstElement(UIElement rootElement, Type typeToFind)
{
bool found = false;
UIElement currentElement = rootElement;
var x1 = currentElement.GetType().GetProperty("Content");
if (x1 != null)
{
var content = currentElement.GetType().GetProperty("Content").GetValue(currentElement, BindingFlags.GetProperty, null, null, null);
if (content == null)
return null;
if (content.GetType() == typeToFind)
return content as UIElement;
return FocusOnFirstElement(content as UIElement, typeToFind);
}
var x2 = currentElement.GetType().GetProperty("Child");
if (x2 != null)
{
var child = currentElement.GetType().GetProperty("Child").GetValue(currentElement, BindingFlags.GetProperty, null, null, null);
if (child == null)
return null;
if (child.GetType() == typeToFind)
return child as UIElement;
return FocusOnFirstElement(child as UIElement, typeToFind);
}
var x4 = currentElement.GetType().GetProperty("Items");
if (x4 != null)
{
ItemCollection items = (ItemCollection)currentElement.GetType().GetProperty("Items").GetValue(currentElement, BindingFlags.GetProperty, null, null, null);
if (items == null)
return null;
foreach (var item in items)
{
if (item.GetType() == typeToFind)
return item as UIElement;
UIElement retunedElement = FocusOnFirstElement(item as UIElement, typeToFind);
if (retunedElement != null)
return retunedElement;
}
}
var x5 = currentElement.GetType().GetProperty("Children");
if (x5 != null)
{
UIElementCollection children = (UIElementCollection)currentElement.GetType().GetProperty("Children").GetValue(currentElement, BindingFlags.GetProperty, null, null, null);
if (children == null)
return null;
foreach (var item in children)
{
if (item.GetType() == typeToFind)
return item as UIElement;
UIElement retunedElement = FocusOnFirstElement(item as UIElement, typeToFind);
if (retunedElement != null)
return retunedElement;
}
}
return null;
}
TreeView in a DataTemplate to display an object containing object references
Some objects contain other objects and it's properties. Displaying these objects is often dificult.
A DataTemplate with a TreeView can do this.
We will use an example of a Customer object with several Address objects as properties of the customer.
Create a DataTemplate for this object in a ResourceDirectory file.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Customer="clr-namespace:MyNamespace;assembly=MyNamespace">
<DataTemplate DataType="{x:Type Customer:MyCustomerObject}" x:Key="CustomerObject">
<TreeViewItem IsExpanded="True">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Label >Customer (ID): (</Label>
<Label Width="100" Content="{Binding Id}"/>
<Label >) </Label>
<Label Content="{Binding LastName}"/>
<Label >, </Label>
<Label Content="{Binding FirstName}"/>
</StackPanel>
</TreeViewItem.Header>
<TreeViewItem.Items>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Email: " />
<Label Content=" " />
<Label Content="{Binding Email}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Mobile Phone: " />
<Label Content=" " />
<Label Content="{Binding MobilePhone}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Work Phone: " />
<Label Content=" " />
<Label Content="{Binding WorkPhone}"/>
</StackPanel>
</StackPanel>
<!-- Address 1 -->
<TreeViewItem ItemsSource="{Binding Address1}" IsExpanded="True">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Label Content="Home Address:"/>
</StackPanel>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Street}" />
<Label Content=" " />
<Label Content="{Binding StreetNumber}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding City}"/>
<Label Content=" " />
<Label Content="{Binding PostCode}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Country}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
<!-- Address 2 -->
<TreeViewItem ItemsSource="{Binding Address2}" IsExpanded="True">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Label Content="Work Address:"/>
</StackPanel>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Street}" />
<Label Content=" " />
<Label Content="{Binding StreetNumber}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding City}"/>
<Label Content=" " />
<Label Content="{Binding PostCode}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Country}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</TreeViewItem.Items>
</TreeViewItem>
</DataTemplate>
The Customer and Address objects would look something like this:
class MyCustomerObject
{
public string Name { get; set; }
public string Email { get; set; }
public string MobilePhone { get; set; }
public string WorkPhone { get; set; }
public MyAddress Address1 { get; set; }
public MyAddress Address2 { get; set; }
}
class MyAddress
{
public string Street { get; set; }
public string StreetNumber { get; set; }
public string City { get; set; }
public string PostCode { get; set; }
public string Country { get; set; }
}
You can set the DataTemplate for the object in code using the ContentTemplate property:
ScrollViewer sv = new ScrollViewer();
sv.ContentTemplate = FindResource("CustomerObject") as DataTemplate;
sv.Content = customer;
For the resource to be found, you have to specify it's location in either the App.xaml or the page where it is used:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/CustomerTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
DoEvents() – a tiny method of joy
Updating the UI while you are processing data and doing other things is very easily done using the DoEvents() method.
This little wonderful method is found in System.Windows.Forms.Application
A good example of how to use it....
You are looping thru 1000 records and each record take perhaps a few huntred milisecconds.
At the bottom of the loop add the the following:
foreach (var x in xxxxxx)
{
...
...
...
...
...
UpdateStatusBar(); // or some method that will give an update to the user
System.Windows.Forms.Application.DoEvents();
}
Each time thru the loop the UI will be updated and the user will see the progress.
Excellent!
Binding to a TextBox programmatically
We start with the text box in the XAML:
<TextBox x:Name="tbName" Text="{Binding Name, UpdateSourceTrigger = PropertyChanged}" />
There are 2 important items here.
The 1st is the Binding to the dependancy property declared in the code behind: 'Binding Name'
The 2nd is the UpdateSourceTrigger. Setting it to 'PropertyChanged' triggers the binding whenever the content in the box has been chagned.
Without this, the binding is only updated on the LostFocus event. You would have to click on another form element or somehow change the focus for the changes in the text box to be noticed.
A 3rd property (not shown here) is the Binding Mode. The default for a TextBox is 'TwoWay' so it was not necessary to add it. The other choices are 'OneWay', etc...
In the defination of the XAML file, the DataContext must be defined:
DataContext="{Binding RelativeSource = {RelativeSource Self}}"
Forget this and it will not know where to find the 'Name' property defined in the text box binding.
Define the DependencyProperty in the code behind page of the user control.
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof (string),
typeof (MyUserControl), new UIPropertyMetadata(string.Empty));
When you add 'MyUserControl' to a window or control programmatically, the binding must be spelled out like this:
MyUserControl myUserControl = new MyUserControl(); // Create a new instance of the user control Binding nameBinding= new Binding(); // Create a new binding nameBinding.Source = personObject; // The source of the the object itself, not the specific property of the object nameBinding.Path = new PropertyPath(personObject.Name); // The path is specifies what property in the object is bound nameBinding.Mode = BindingMode.TwoWay; // Again the mode is set to TwoWay nameBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; // Again the trigger is set to PropertyChange myUserControl.SetBinding(MyUserControl.NameProperty, nameBinding); // then the binding is set to the dependency property
You can see that several properties (Trigger and Mode) are set both here and in the XAML
Background workers
Background workers are VERY useful. They keep you to do run intensive process without locking up your UI.
Again, a picture (or code) is worth a thousand words...
// Declare your background worker
private BackgroundWorker bgWorker = new BackgroundWorker();
// In the constructor or some other method you must initialize things
private void StartBackgroundWorker()
{
// When the background worker finishes, this event is triggered
bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgWorker_RunWorkerCompleted);
// When the background worker is started, this event is triggered
bgWorker.DoWork += new DoWorkEventHandler(bgWorker_DoWork);
// If the background worker is canceled, this event is triggered
bgWorker.WorkerSupportsCancellation = true;
// Create a button someplace to stop the background worker
btnStop.Click += new RoutedEventHandler(btnStop_Clicked);
// start the background worker
try
{
bgWorker.RunWorkerAsync();
}
catch (Exception ex) { // do something }
}
private void btnStop_Clicked(object sender, RoutedEventArgs e)
{
// Unregister the events or they may not get garbage collected
btnStop.Click -= new RoutedEventHandler(btnStop_Clicked);
bgWorker.RunWorkerCompleted -= new RunWorkerCompletedEventHandler(bgWorker_RunWorkerCompleted);
bgWorker.DoWork -= new DoWorkEventHandler(bgWorker_DoWork);
// stop the background worker
bgWorker.CancelAsync();
}
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Start the process
// This will be done in a seperate thread
}
private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
return;
}
if (e.Error != null)
{
return;
}
// show the user the results of the process that was running... or whatever you want to do
// Unregister the events or they may not get garbage collected
btnStop.Click -= new RoutedEventHandler(btnStop_Clicked);
bgWorker.RunWorkerCompleted -= new RunWorkerCompletedEventHandler(bgWorker_RunWorkerCompleted);
bgWorker.DoWork -= new DoWorkEventHandler(bgWorker_DoWork);
}
That is all there is to it! Simple? Sure it is.
However, there are some things you should know.
* If you cancel the background worker, they process is not stopped. It will continue running until it finishes. Canceling simple disconnects that thread and stops listening to it.
* If you want to stop that thread, then you should have that thread often check for the cancel event and stop itself if it is TRUE.
* If you start and cancel a process too many time, you can will ecounter problems. You should develope a way to prevent this.
Routed Events – Bubbling
Because I sometimes forget the syntax, a have here a quick bubbling routed event for WPF:
For you have to register the event. Then you have to create the poperty that adds and removed the handlers.
// Register the event
public static readonly RoutedEvent CloseEvent =
EventManager.RegisterRoutedEvent("Close", RoutingStrategy.Bubble, typeof(RoutedEventHandler),
typeof(CustomPopupControl));
// Provide CLR accessors for the event
public event RoutedEventHandler Close
{
add { AddHandler(CloseEvent, value); }
remove { RemoveHandler(CloseEvent, value); }
}
To manualy raise the event:
RoutedEventArgs args = new RoutedEventArgs(CloseEvent); RaiseEvent(args);
To listen for the event:
CustomPopupControl popup = new CustomerPopupControl(); popup.Close += new RoutedEventHandler(popup_Closed);