Can I make this block of XAML into a reusable "control"? - c#

I have a Grid, and in that grid, I have this:
<StackPanel Grid.Row="2"
Grid.Column="0">
<Grid x:Name="GridButtonItem" Margin="30,0,0,5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Background"
Value="Transparent" />
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
Value="#332a8dd4" />
</Trigger>
<Trigger Property="IsMouseOver"
Value="False">
<Setter Property="Background"
Value="Transparent" />
</Trigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Image Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Margin="3"
Source="{dx:DXImageOffice2013 Image=Windows_32x32.png}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<TextBlock Grid.Row="0"
Grid.Column="1"
Margin="10,3,3,0"
Text="Application Log" />
<TextBlock Grid.Row="1"
Grid.Column="1"
Margin="10,0,3,3"
Text="C:\Program Files (x86)\ATI Technologies\ATI.ACE\MOM-InstallProxy" />
</Grid>
</StackPanel>
The StackPanel is actually meant to hold many of the GridButtonItem items. Is there a way that I can somehow make a "template" of GridButtonItem and then for each one I want to add to the StackPanel, just set the Image and Text properties?
Something like this (just pseudo-code for demonstration):
<StackPanel>
<Grid Template="myGridItemTemplate">
<Setter Property="Image" Value="img1.png"/>
<Setter Property="Text1" Value="button1 Text"/>
<Setter Property="Text2" Value="button2 Text"/>
</Grid>
<Grid Template="myGridItemTemplate">
<Setter Property="Image" Value="img1.png"/>
<Setter Property="Text1" Value="button1 Text"/>
<Setter Property="Text2" Value="button2 Text"/>
</Grid>
<Grid Template="myGridItemTemplate">
<Setter Property="Image" Value="img1.png"/>
<Setter Property="Text1" Value="button1 Text"/>
<Setter Property="Text2" Value="button2 Text"/>
</Grid>
</StackPanel>
So each one that is added picks up the row/column definitions, and an embedded Image and two TextBlocks. Then I just set the three properties for each one added.
Is this possible?

You can put your grid control into a UserControl and then reuse the UserControl throughout your project. I have a simple example of doing this with a label and Textbox.
here is the XAML:
<UserControl x:Class="TestVision.CustomControls.LabelAndTextbox"
x:Name="parent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestVision.CustomControls"
mc:Ignorable="d" >
<StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parent}">
<TextBlock Text="{Binding Path=Label}" Width="{Binding Path=LabelWidth}" VerticalAlignment="Center" TextAlignment="Right" Margin="0,0,10,0" Height="22"/>
<TextBox Text="{Binding Path=Text, UpdateSourceTrigger=PropertyChanged}" Width="{Binding Path=TextboxWidth}" IsReadOnly="{Binding Path=TextboxReadOnly, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="{Binding Path=TextboxHorizontalContentAlgnment}"/>
</StackPanel>
</UserControl>
Any properties that you want to be able to set e.g. your image text etc. must be bound to Dependency Properties in the code behind.
Code behind:
public partial class LabelAndTextbox : UserControl
{
/// <summary>
/// Gets or sets the Label which is displayed next to the field
/// </summary>
public String Label
{
get { return (String)GetValue(LabelContent); }
set { SetValue(LabelContent, value); }
}
/// <summary>
/// Identified the Label dependency property
/// </summary>
public static readonly DependencyProperty LabelContent =
DependencyProperty.Register("Label", typeof(string),
typeof(LabelAndTextbox), new PropertyMetadata(""));
public object Text
{
get { return (object)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(object),
typeof(LabelAndTextbox), new PropertyMetadata(null));
public Double LabelWidth
{
get { return (Double)GetValue(LabelWidthProperty); }
set { SetValue(LabelWidthProperty, value); }
}
public static readonly DependencyProperty LabelWidthProperty =
DependencyProperty.Register("LabelWidth", typeof(Double),
typeof(LabelAndTextbox), new PropertyMetadata());
public Double TextboxWidth
{
get { return (Double)GetValue(TextboxWidthProperty); }
set { SetValue(TextboxWidthProperty, value); }
}
public static readonly DependencyProperty TextboxWidthProperty =
DependencyProperty.Register("TextboxWidth", typeof(Double),
typeof(LabelAndTextbox), new PropertyMetadata());
public bool TextboxReadOnly
{
get { return (bool)GetValue(TextboxReadOnlyProperty); }
set { SetValue(TextboxReadOnlyProperty, value); }
}
public static readonly DependencyProperty TextboxReadOnlyProperty =
DependencyProperty.Register("TextboxReadOnly", typeof(bool),
typeof(LabelAndTextbox), new FrameworkPropertyMetadata());
public HorizontalAlignment TextboxHorizontalContentAlgnment
{
get { return (HorizontalAlignment)GetValue(TextboxHorizontalContentAlgnmentProperty); }
set { SetValue(TextboxHorizontalContentAlgnmentProperty, value); }
}
public static readonly DependencyProperty TextboxHorizontalContentAlgnmentProperty =
DependencyProperty.Register("TextboxHorizontalContentAlgnment", typeof(HorizontalAlignment),
typeof(LabelAndTextbox), new FrameworkPropertyMetadata());
public LabelAndTextbox()
{
InitializeComponent();
}
}
you then will need to add a reference in the XAML file to your UserControl like this:
xmlns:Resource="clr-namespace:ProjectNamespace.FolderContainingYourControl"
Resource is a generic identifier you can call it what you like, you can then reference your control in the like this:
<Resource:LabelAndTextblock x:Name="AddressLine1" Label="{Binding LblTxt_AddressLine1}" Text="{Binding AddressLine1, Mode=TwoWay}" Margin="10,5,0,5" LabelWidth="70" TextWidth="250" TextHeight="60"/>

You could do this with a UserControl (two different ways) or a DataTemplate. Let's go with DataTemplate, because stuicidle already ably demonstrated one UserControl approach.
There are a couple of different ways to do this with a DataTemplate, too.
We're going to do something called an implicit DataTemplate. It's created in Resources, but it has no x:Key property, just a DataType="{x:Type local:GridItemViewModel}" property. What that will do is this: Wherever that DataTemplate is in scope, whenever XAML needs to display a GridItemViewModel and nothing is specifying a template to display it in, it'll use that implicit template.
Clear as mud! Welcome to the XAML learning curve.
ViewModels.cs
using System;
using System.ComponentModel;
using System.Windows.Media;
namespace GridItemAnswer
{
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
#region GridItemViewModel Class
public class GridItemViewModel : ViewModelBase
{
#region LabelText Property
private String _labelText = null;
public String LabelText
{
get { return _labelText; }
set
{
if (value != _labelText)
{
_labelText = value;
OnPropertyChanged();
}
}
}
#endregion LabelText Property
#region Path Property
private String _path = null;
public String Path
{
get { return _path; }
set
{
if (value != _path)
{
_path = value;
OnPropertyChanged();
}
}
}
#endregion Path Property
#region ImageSource Property
private ImageSource _imageSource = null;
public ImageSource ImageSource
{
get { return _imageSource; }
set
{
if (value != _imageSource)
{
_imageSource = value;
OnPropertyChanged();
}
}
}
#endregion ImageSource Property
}
#endregion GridItemViewModel Class
}
MainWindow.xaml
<Window
x:Class="GridItemAnswer.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:GridItemAnswer"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate DataType="{x:Type local:GridItemViewModel}">
<StackPanel>
<Grid x:Name="GridButtonItem" Margin="30,0,0,5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#332a8dd4" />
</Trigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Image
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Margin="3"
Source="{Binding Image}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="10,3,3,0"
Text="{Binding LabelText}"
/>
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="10,0,3,3"
Text="{Binding Path}"
/>
</Grid>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel>
<ItemsControl>
<local:GridItemViewModel
LabelText="Foo Bar"
Path="c:\foo\bar"
/>
<local:GridItemViewModel
LabelText="Baz Planxty"
Path="c:\baz\planxty"
/>
</ItemsControl>
<Label>
<local:GridItemViewModel
LabelText="A frog walks into a bank asking for a loan"
Path="c:\knick\knack"
/>
</Label>
</StackPanel>
</Grid>
</Window>

Related

LiveCharts derived user control with data binding displays no chart

I am creating a C# based WPF demonstration application that features a user defined control (User Control) "card" that features two labels and a chart (LiveCharts).
How do I correctly have the data bound to the LiveCharts control inside the card user control so the chart displays?
I have tried a variety of possible solutions including the DataContext={Binding RelativeSource={RelativeSource Self}}.
Without using the user control, the chart displays correctly, but as I have more than one instance required, I wish to have a user control for reuse.
Picture: Sample of current application when running
Repository
https://github.com/PWA-GouldA/C4Prog-DotNet-WPF-LiveChartDemo
Code Extract:
Interaction logic for CardLineChart.xaml
public partial class CardLineChart : UserControl
{
public CardLineChart()
{
InitializeComponent();
CardGrid.DataContext = this;
}
#region SeriesData DP
public SeriesCollection SeriesData
{
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("SeriesData",
typeof(SeriesCollection), typeof(CardLineChart),
new PropertyMetadata(new SeriesCollection()));
#endregion
#region BackgroundColour DP
public string BackgroundColour
{
set { SetValue(ColourBG, value); }
}
public static readonly DependencyProperty ColourBG =
DependencyProperty.Register("BackgroundColour",
typeof(string), typeof(CardLineChart), new PropertyMetadata(""));
#endregion
#region BottomLabel DP
public string BottomLabel
{
set { SetValue(LabelAtBottom, value); }
}
public static readonly DependencyProperty LabelAtBottom =
DependencyProperty.Register("BottomLabel",
typeof(string), typeof(CardLineChart), new PropertyMetadata(null));
#endregion
#region TopLabel DP
public string TopLabel
{
set { SetValue(LabelAtTop, value); }
}
public static readonly DependencyProperty LabelAtTop =
DependencyProperty.Register("TopLabel",
typeof(string), typeof(CardLineChart), new PropertyMetadata(null));
#endregion
}
CardLineChart.xaml
<UserControl x:Class="WPF_With_LiveCharts.CardLineChart"
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:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="200"
>
<Grid x:Name="CardGrid" Margin="5,5,5,5" MaxHeight="200" MaxWidth="200">
<Grid.Effect>
<DropShadowEffect BlurRadius="15" Direction="-90" RenderingBias="Quality" Opacity=".2" ShadowDepth="2"/>
</Grid.Effect>
<Grid.OpacityMask>
<VisualBrush Visual="{Binding ElementName=Border1}" />
</Grid.OpacityMask>
<Grid.Resources>
<Style TargetType="lvc:LineSeries">
<Setter Property="StrokeThickness" Value="1"></Setter>
<Setter Property="Stroke" Value="White"></Setter>
<Setter Property="Fill" Value="#00ffffff"></Setter>
<Setter Property="PointGeometrySize" Value="0"></Setter>
<Setter Property="LineSmoothness" Value="0.25"></Setter>
</Style>
<Style TargetType="lvc:Axis">
<Setter Property="ShowLabels" Value="False"></Setter>
<Setter Property="IsEnabled" Value="False"></Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="60"></RowDefinition>
<RowDefinition Height="80*"></RowDefinition>
</Grid.RowDefinitions>
<Border x:Name="Border1" Grid.Row="0" Grid.RowSpan="3" CornerRadius="5"
Background="{Binding Path=BackgroundColour}" />
<TextBlock Grid.Row="0" TextAlignment="Center" Padding="5, 5, 0, 5"
Foreground="#ccFFFFFF" FontSize="12"
Text="{Binding Path=TopLabel}" />
<lvc:CartesianChart Grid.Row="1"
Margin="0,0,0,0"
Series="{Binding Path=SeriesData}"
Hoverable="False"
DataTooltip="{x:Null}">
<lvc:CartesianChart.AxisX>
<lvc:Axis MinValue="0"></lvc:Axis>
</lvc:CartesianChart.AxisX>
</lvc:CartesianChart>
<TextBlock Grid.Row="2" x:Name="ChartValue"
Foreground="#ccFFFFFF" FontSize="48"
VerticalAlignment="Center" TextAlignment="Center"
Margin="8,0,8,6" Text="{Binding Path=BottomLabel}"/>
</Grid>
</UserControl>
WindowMain.cs
public partial class MainWindow : Window
{
public SeriesCollection theData;
public SeriesCollection theData2;
public string BackgroundColour;
public MainWindow()
{
InitializeComponent();
theData = new SeriesCollection
{
new LineSeries
{
Values = new ChartValues<decimal> { 7, 3, 2, 3, 5, 7, 4 }
}
};
theData2 = new SeriesCollection
{
new LineSeries
{
Values = new ChartValues<decimal> { 100, 90, 70, 40, 10 }
}
};
BackgroundColour = "#FFCE2156";
DataContext = this;
}
private void ButtonLineChart_Click(object sender, RoutedEventArgs e)
{
}
}
WindowMain.xaml
<Window x:Class="WPF_With_LiveCharts.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:WPF_With_LiveCharts"
xmlns:control = "clr-namespace:WPF_With_LiveCharts"
mc:Ignorable="d"
Title="MainWindow" Height="460" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="45"/>
<RowDefinition Height="200"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
</Grid.ColumnDefinitions>
<Button x:Name="ButtonLineChart" Content="Line Chart" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Click="ButtonLineChart_Click" Height="25"/>
<Button x:Name="ButtonPieChart" Content="Pie Chart" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Grid.Column="2" Height="25"/>
<control:CardLineChart
Grid.Column="0" Grid.Row="1"
x:Name="TestingCardLineChart"
SeriesData="{Binding theData}"
BackgroundColour="#ffc22735"
TopLabel="Testing"
BottomLabel="123"
/>
<control:CardLineChart
Grid.Column="1"
Grid.Row="1"
x:Name="TestingCardLineChart2"
SeriesData="{Binding theData2}"
BackgroundColour="#FF000000"
TopLabel="Oh Yes!"
BottomLabel="9999"
/>
</Grid>
</Window>
Binding doesn't work on fields in WPF. If you change the series collection on MainWindow.xaml.cs to use properties instead you should be able to see the graphs.
eg
public SeriesCollection theData { get; set; }
public SeriesCollection theData2 { get; set; }

How do you get WPF validation to bubble up to a parent control?

So I have a control like this simplified version:
<local:ImageMapField x:Class="ImageApp.WPF.Controls.ImageMapContentField"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ImageApp.WPF.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
x:Name="Me">
<Grid HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" HorizontalAlignment="Stretch" Style="{DynamicResource BaseLabelStyle}">
<TextBlock Text="{Binding Header, RelativeSource={RelativeSource AncestorType=local:ImageMapContentField, Mode=FindAncestor}}" TextWrapping="WrapWithOverflow"></TextBlock>
</Label>
<StackPanel Grid.Column="1">
<Image />
<Border Margin="20,5,5,2">
<ContentPresenter Content="{Binding DataEntryContent, ElementName=Me}" />
</Border>
</StackPanel>
</Grid>
</local:ImageMapField>
And am using it like so:
<controls:ImageMapContentField Header="Foo Date"
FieldName="FooDate"
ImageSource="{Binding MyImage, Mode=TwoWay}"
ItemsSource="{Binding Map.Items}"
Zoom="{Binding MapFieldZoom}">
<controls:ImageMapContentField.DataEntryContent>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding MyDate, StringFormat=MM/dd/yyyy, ValidatesOnDataErrors=True, NotifyOnValidationError=True}">
<controls:WatermarkService.Watermark>
<TextBlock>Date</TextBlock>
</controls:WatermarkService.Watermark>
</TextBox>
<TextBox Grid.Column="1" Text="{Binding MyTime, StringFormat=HH\:mm}">
<controls:WatermarkService.Watermark>
<TextBlock>Time</TextBlock>
</controls:WatermarkService.Watermark>
</TextBox>
</Grid>
</controls:ImageMapContentField.DataEntryContent>
</controls:ImageMapContentField>
The problem is that because I am not binding my model's property to something on the ImageMapContentField, Validation.HasError on the ImageMapContentField is always false and never triggers.
What I get instead is the default TextBox validation.
What I really want is the ImageMapContentField to have a pink background. This works for my other controls where I am binding directly to something, but I cannot get this to work for the controls that have a ContentPresenter.
I am hoping I am just missing something that would allow the parent to capture the validation.
As requested here is a minimal example of the issue:
MainWindow.xaml
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="local:CustomTextField">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
<Setter Property="Background" Value="LightPink"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="local:CustomContentControl">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
<Setter Property="Background" Value="LightPink"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Window.DataContext>
<local:MyModel />
</Window.DataContext>
<StackPanel>
<local:CustomTextField LabelText="Number 1" Value="{Binding Number1, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True}" />
<local:CustomContentControl LabelText="Number 2">
<local:CustomContentControl.DataEntryContent>
<TextBox Text="{Binding Number2, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True}" />
</local:CustomContentControl.DataEntryContent>
</local:CustomContentControl>
</StackPanel>
</Window>
CustomTextField.xaml
<UserControl x:Class="WpfApp1.CustomTextField"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
x:Name="Me">
<StackPanel>
<Label Content="{Binding ElementName=Me, Path=LabelText}" />
<TextBox Text="{Binding ElementName=Me, Path=Value}" />
</StackPanel>
</UserControl>
CustomTextField.cs
public partial class CustomTextField : UserControl
{
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register(
"LabelText", typeof(string), typeof(CustomTextField), new PropertyMetadata(default(string)));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value", typeof(string), typeof(CustomTextField), new PropertyMetadata(default(string)));
public string Value
{
get { return (string) GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public string LabelText
{
get { return (string) GetValue(LabelTextProperty); }
set { SetValue(LabelTextProperty, value); }
}
public CustomTextField()
{
InitializeComponent();
}
}
CustomContentControl.xaml
<UserControl x:Class="WpfApp1.CustomContentControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
x:Name="Me">
<Grid>
<StackPanel>
<Label Content="{Binding ElementName=Me, Path=LabelText}" />
<ContentPresenter Content="{Binding DataEntryContent, ElementName=Me}" />
</StackPanel>
</Grid>
</UserControl>
CustomContentControl.cs
public partial class CustomContentControl : UserControl
{
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register(
"LabelText", typeof(string), typeof(CustomContentControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty DataEntryContentProperty = DependencyProperty.Register(
"DataEntryContent", typeof(object), typeof(CustomContentControl), new PropertyMetadata(default(object)));
public object DataEntryContent
{
get { return (object) GetValue(DataEntryContentProperty); }
set { SetValue(DataEntryContentProperty, value); }
}
public string LabelText
{
get { return (string) GetValue(LabelTextProperty); }
set { SetValue(LabelTextProperty, value); }
}
public CustomContentControl()
{
InitializeComponent();
}
}
MyModel.cs
public class MyModel : INotifyPropertyChanged
{
int _number1;
int _number2;
public int Number1
{
get { return _number1; }
set
{
_number1 = value;
OnPropertyChanged();
}
}
public int Number2
{
get { return _number2; }
set
{
_number2 = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The WPF validation is already bubbling up to the parent control (even when the child control is inside a ContentPresenter) - Validation.ErrorEvent
The problem here is that even though the event bubbles up, the attached property Validation.HasError doesn't get updated - that's basically due to the fact that there is no error in the control's property bindings. And hence, you don't see the background change.
To rectify this - you can use this code:
Update style in MainWindow.xaml
<Style TargetType="local:CustomContentControl">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
<AdornedElementPlaceholder/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="HasErrors" Value="True">
<Setter Property="Background" Value="LightPink"/>
</Trigger>
</Style.Triggers>
</Style>
And, update CustomContentControl to add a HasErrors dependency property, and validation error event handler
public static readonly DependencyProperty HasErrorsProperty = DependencyProperty.Register("HasErrors", typeof(bool), typeof(CustomContentControl), new PropertyMetadata(false));
public bool HasErrors
{
get { return (bool)GetValue(HasErrorsProperty); }
set { SetValue(HasErrorsProperty, value); }
}
public CustomContentControl()
{
InitializeComponent();
Validation.AddErrorHandler(this, (s, args) => {
if (args.Action == ValidationErrorEventAction.Added)
{
this.ToolTip = args.Error.ErrorContent;
HasErrors = true;
}
else
{
this.ToolTip = null;
HasErrors = false;
}
});
}
And your background will be updated.

WPF Custom Control based on grid or nested grid?

I'm learning WPF but I have a lot of Windows Forms background. I want to convert a WinForms custom control in which I did put a label and a textbox (making a TextField), with a property allowing to set percentage of width allocated to the label.
Now, in WPF, I'm a bit lost. Should I create a custom control that inherits from a grid and expose (how ?) the columns definition properties, or should I create a custom control that will "contain" a grid, and expose two properties "LabelWidth" and "ContentWidth", and bind the two column definitions to these properties ? (Thinking these properties would contain 1* and 3*).
Could someone show me an example of such construction to have a place to start?
You could create a UserControl with two dependency properties.
Please refer to the following sample code.
MyUserControl.xaml:
<UserControl x:Class="WpfApplication3.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication3"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding LabelWidth, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<ColumnDefinition Width="{Binding ContentWidth, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</Grid.ColumnDefinitions>
<TextBlock Text="..." />
<TextBox Grid.Column="1" />
</Grid>
</UserControl>
MyUserControl.xaml.cs:
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty LabelWidthProperty =
DependencyProperty.Register("LabelWidth", typeof(System.Windows.GridLength),
typeof(MyUserControl));
public System.Windows.GridLength LabelWidth
{
get { return (System.Windows.GridLength)GetValue(LabelWidthProperty); }
set { SetValue(LabelWidthProperty, value); }
}
public static readonly DependencyProperty ContentWidthProperty =
DependencyProperty.Register("ContentWidth", typeof(System.Windows.GridLength),
typeof(MyUserControl));
public System.Windows.GridLength ContentWidth
{
get { return (System.Windows.GridLength)GetValue(ContentWidthProperty); }
set { SetValue(ContentWidthProperty, value); }
}
}
Sample usage:
<local:MyUserControl LabelWidth="1*" ContentWidth="5*" />
Dependency Properties Overview: https://msdn.microsoft.com/en-us/library/ms752914(v=vs.110).aspx
I think I managed to achieve what I wanted to do by understanding mm8's code, in particular RelativeSource={RelativeSource AncestorType=UserControl} :
Added a custom control.
FieldText.cs :
public class FieldText : Control
{
static FieldText()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FieldText), new FrameworkPropertyMetadata(typeof(FieldText)));
}
public FieldText()
{
}
public static readonly DependencyProperty LabelLengthProperty =
DependencyProperty.Register("LabelLength", typeof(GridLength),
typeof(FieldText), new UIPropertyMetadata(new GridLength(25, GridUnitType.Star)));
public virtual GridLength LabelLength
{
get { return (GridLength)GetValue(LabelLengthProperty); }
set { SetValue(LabelLengthProperty, value); }
}
public static readonly DependencyProperty ContentLengthProperty =
DependencyProperty.Register("ContentLength", typeof(GridLength),
typeof(FieldText), new UIPropertyMetadata(new GridLength(75, GridUnitType.Star)));
public virtual GridLength ContentLength
{
get { return (GridLength)GetValue(ContentLengthProperty); }
set { SetValue(ContentLengthProperty, value); }
}
}
Generic.xaml :
<Style TargetType="{x:Type controls:FieldText}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:FieldText}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="grd" Margin="3px">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding Path=LabelLength, RelativeSource={RelativeSource AncestorType=Control}}" />
<ColumnDefinition Width="{Binding Path=ContentLength, RelativeSource={RelativeSource AncestorType=Control}}" />
</Grid.ColumnDefinitions>
<Label x:Name="label" Grid.Column="0" Content="Field:" />
<TextBox x:Name="textbox" Grid.Column="1" MaxLines="1" TextWrapping="NoWrap" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Sample usage:
<controls:FieldText x:Name="fld1" LabelLength="25*" ContentLength="75*" />

OK button in dialog not enabling

I am building a WPF application using MVVM Light. In it I have a dialog box. The XAML:
<Window x:Class="ParserEditor.NewParserDialog"
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:ParserEditor"
xmlns:ignore="http://www.galasoft.ch/ignore"
mc:Ignorable="d ignore"
DataContext="{Binding NewParser, Source={StaticResource Locator}}"
Title="New Parser..."
SizeToContent="Height"
Width="300">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
<BitmapImage x:Key="ErrorImage" UriSource="Resources/Error.png" />
<local:BooleanToVisibilityConverter x:Key="BoolToVisiblity" True="Visible" False="Collapsed" />
<ControlTemplate x:Key="InputErrorTemplate">
<DockPanel LastChildFill="True">
<Image DockPanel.Dock="Right"
Height="16"
Margin="5"
Source="{StaticResource ErrorImage}"
ToolTip="Contains invalid data"
VerticalAlignment="Center"
Width="16" />
<Border BorderBrush="Red"
BorderThickness="2">
<AdornedElementPlaceholder />
</Border>
</DockPanel>
</ControlTemplate>
<Style TargetType="ComboBox">
<Setter Property="Margin" Value="5,4,26,4" />
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource InputErrorTemplate}" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding Path="(Validation.Errors).CurrentItem.ErrorContent" RelativeSource="{x:Static RelativeSource.Self}" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Name="PromptLabel"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Text="{Binding Path=Prompt, Mode=TwoWay}"
Visibility="{Binding Path=HasPrompt, Converter={StaticResource BoolToVisiblity}}"/>
<TextBlock Name="ParserTypeLabel"
Grid.Column="0"
Grid.Row="2"
Text="Parser Type:" />
<ComboBox Name="ParserTypePicker"
Grid.Column="1"
Grid.Row="2"
ItemsSource="{Binding Path=ParserTypes}"
SelectedItem="{Binding Path=ParserType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
<Grid Name="ButtonGrid"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Name="OkButton"
Command="{Binding CloseCommand, ValidatesOnDataErrors=True}"
Grid.Column="0"
Content="OK"
IsDefault="True" />
<Button Name="CancelButton"
Grid.Column="1"
Content="Cancel"
IsCancel="True" />
</Grid>
</Grid>
</Window>
The View Model object:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
using ParserEditor.Model;
namespace ParserEditor.ViewModel {
public class NewParserViewModel : ViewModelBase, IDataErrorInfo {
private readonly IDataService _dataService;
public string ParserType {
get { return _ParserType; }
set { Set( ref _ParserType, value ); }
}
private string _ParserType;
public ObservableCollection<string> ParserTypes { get; private set; }
public bool HasPrompt {
get { return !string.IsNullOrWhiteSpace( Prompt ); }
}
public string Prompt {
get { return _Prompt; }
set {
Set( ref _Prompt, value );
RaisePropertyChanged( nameof( HasPrompt ) );
}
}
private string _Prompt;
#region CloseCommand
public RelayCommand CloseCommand { get; private set; }
private bool CanCloseDialog() {
return ParserType == DataService.AWK_FORMAT ||
ParserType == DataService.CSHARP_FORMAT ||
ParserType == DataService.REGEX_FORMAT;
}
private void CloseDialog() {
Messenger.Default.Send( new CloseWindowMessage() );
}
#endregion
#region IDataErrorInfo Implementation
public string Error {
get { return this[ "ParserType" ]; }
}
public string this[ string columnName ] {
get {
switch ( columnName ) {
case "ParserType":
return string.IsNullOrWhiteSpace( ParserType ) ? "You must choose a Parser Type" : null;
default:
return null;
}
}
}
#endregion
public NewParserViewModel( IDataService dataService ) {
_dataService = dataService;
CloseCommand = new RelayCommand( CloseDialog, CanCloseDialog );
ParserTypes = new ObservableCollection<string>();
ParserTypes.Add( DataService.AWK_FORMAT );
ParserTypes.Add( DataService.CSHARP_FORMAT );
ParserTypes.Add( DataService.REGEX_FORMAT );
}
}
}
I've placed a breakpoint in the CanCloseDialog method and it only gets hit once, when the dialog is first displayed. If I select a choice in the ComboBox after the dialog is displayed, the OK button doesn't enable.
What am I missing?
I did some more searching & I finally found the answer here. It turns out I had to change the
using GalaSoft.MvvmLight.Command;
statement to
GalaSoft.MvvmLight.CommandWpf;
Doing this, everything works properly.
Try a delegate Func wich call CanCloseDialog instead.
Somehow RelayCommand lose pointer to the method whithout a delegate.
Invoke the delegate in the CanExecute method of the RelayCommand implementation.
Something like :
CloseCommand = new RelayCommand( CloseDialog,() => {return <your condition logic>});

Binding from a property of each item inside an ItemsControl to a property of an object outside the ItemsControl

I have an ItemsControl, and a Button outside the ItemsControl. Each item inside the ItemsControl has a dependency property called "MyProperty" (defined in the code-behind).
I would like to set the IsEnabled property of the Button to false when at least one of the items in the ItemsControl has the MyProperty property set to 5. (of course this is just a stupid example of a more complicated situation)
I tried by means of a data trigger, but with no luck:
XAML:
<Window x:Class="cancellami24.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style x:Key="MyStyle" TargetType="{x:Type ContentPresenter}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MyProperty}" Value="5">
<Setter Property="IsEnabled" TargetName="MyButton" Value="False" /><!--error on TargetName-->
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl x:Name="MyListBox" Grid.Row="0" ItemContainerStyle="{StaticResource MyStyle}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=MyProperty, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button x:Name="MyButton" Grid.Row="1" Click="MyButton_Click"/>
</Grid>
</Window>
Code-behind:
using System.Collections.ObjectModel;
using System.Windows;
namespace cancellami24
{
public partial class MainWindow : Window
{
private readonly ObservableCollection<MyItem> myCollection = new ObservableCollection<MyItem>();
public MainWindow()
{
InitializeComponent();
myCollection.Add(new MyItem(1));
myCollection.Add(new MyItem(2));
myCollection.Add(new MyItem(3));
MyListBox.ItemsSource = myCollection;
}
private void MyButton_Click(object sender, RoutedEventArgs e)
{
myCollection[2].SetValue(MyItem.MyPropertyProperty, 5);
}
}
public class MyItem : DependencyObject
{
public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register("MyProperty", typeof(int), typeof(MyItem));
public MyItem(int propertyValue)
{
SetValue(MyPropertyProperty, propertyValue);
}
}
}
You need custom converter to solve it
public class MyConverter : IValueConverter
{
bool flag = false;
var collection = value as ObservableCollection<MyItem>();
if(collection==null) return flag;
foreach (var item in collection)
{
if (item.MyProperty==5)
{
flag = true;
break;
}
}
return flag;
}
Add MyConverter to your App.xaml
<local:MyConverter x:key="MyConverter"/>
Xaml:
<Button x:Name="MyButton" IsEnabled="{Binding ElementName=MyListBox, Path=ItemsSource, Converter={StaticResource MyConverter}}"/>

Categories