I would like to create a custom control that simplifies the following code:
<StackPanel>
<DockPanel LastChildFill="True">
<Label>First Name</Label>
<TextBox Margin="2" Text="{Binding Path=FirstName}"></TextBox>
</DockPanel>
<DockPanel LastChildFill="True">
<Label>Last Name</Label>
<TextBox Margin="2" Text="{Binding Path=LastName}"></TextBox>
</DockPanel>
</StackPanel>
My thoughts was to make a UserControl like the following, (Layout is a little bit different, but thats out of scope):
<UserControl x:Class="LabelControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<DockPanel LastChildFill="True">
<Label Content="{Binding Path=Text}" Margin="2" MinWidth="100" HorizontalContentAlignment="Right"></Label>
<Grid Margin="2">
<ContentControl Content="{Binding Path=Control}" ></ContentControl>
</Grid>
</DockPanel>
</UserControl>
The code behind exposes 2 Dependency properties:
Text: the content of the label
Control: the control to be hosted by the content.
The class uses the ContentProperty attribute to map the children to the ContentControl.
Thus allowing me to simplify my StackPanel:
<StackPanel>
<controls:LabelControl Text="First Name">
<TextBox Text="{Binding Path=FirstName}"></TextBox>
</controls:LabelControl>
<controls:LabelControl Text="Last Name">
<TextBox Text="{Binding Path=LastName}"></TextBox>
</controls:LabelControl>
</StackPanel>
The problem I am running in to is the bindings in the the control are not mapping. Is there any way around this? The Label Controls DataContext is overridding the parent controls context.
Here is the code behind for the LabelControl:
[ContentProperty("Control")]
public partial class LabelControl : UserControl
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(LabelControl), new PropertyMetadata(default(string)));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty ControlProperty =
DependencyProperty.Register("Control", typeof(Control), typeof(LabelControl), new PropertyMetadata(default(Control)));
public Control Control
{
get { return (Control)GetValue(ControlProperty); }
set { SetValue(ControlProperty, value); }
}
public LabelControl()
{
InitializeComponent();
this.DataContext = this;
}
}
Edit: Output confirms the datacontext is overriding.
BindingExpression path error: 'FirstName' property not found on 'object' ''LabelControl' (Name='')'. BindingExpression:Path=FirstName; DataItem='LabelControl' (Name=''); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Try changing your binding like this if your LabelControl is contained within Window and it's DataContext has the FirstName property.
<TextBox Text="{Binding Path=FirstName,
RelativeSource={RelativeSource AncestorType=Window}}">
</TextBox>
If you don't want to specify RelativeSource every time, you could use your LabelControl as you do now...
<StackPanel>
<controls:LabelControl Text="First Name">
<TextBox Text="{Binding Path=FirstName}"></TextBox>
</controls:LabelControl>
<controls:LabelControl Text="Last Name">
<TextBox Text="{Binding Path=LastName}"></TextBox>
</controls:LabelControl>
</StackPanel>
...and change the LabelControl's implementation instead.
First, loose the DataContext assignment from the LabelControl's codebehind.
public LabelControl()
{
InitializeComponent();
//this.DataContext = this; // we don't want this
}
Then change the XAML template as
<DockPanel LastChildFill="True">
<Label Content="{Binding Path=Text,
RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}"
Margin="2" MinWidth="100" HorizontalContentAlignment="Right">
</Label>
<Grid Margin="2">
<ContentControl
Content="{Binding Path=Control,
RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}">
</ContentControl>
</Grid>
</DockPanel>
Now you should have your DataContext set up right.
I found that using UserControl was not the most ideal solution. It turns out that a templated control allows for the DataBinds to pass through without any hackery (RelativeSource).
[ContentProperty("Control")]
public class LabelControl : Control
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text",
typeof(string), typeof(LabelControl), new PropertyMetadata(default(string)));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty ControlProperty =
DependencyProperty.Register("Control", typeof(Control), typeof(LabelControl), new PropertyMetadata(default(Control)));
public Control Control
{
get { return (Control)GetValue(ControlProperty); }
set { SetValue(ControlProperty, value); }
}
}
In app.xaml:
<Style TargetType="controls:LabelControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:LabelControl">
<DockPanel LastChildFill="True">
<Label Content="{TemplateBinding Text}" MinWidth="100" FontSize="11"></Label>
<Grid Margin="2">
<ContentControl Content="{TemplateBinding Control}"></ContentControl>
</Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Related
Hi I know this is alot of code but I'm hoping someone can help or point me in the right direction my export to csv command isn't firing even when using breakpoints on the command and task so I assume it can't be found in the data context somwhere, everything else works the data gets populated, edit button works.
So I have 2 UserControls TitleControl and BillOfMaterialsControl, I have some buttons hosted in the title control where this is hosted in the bill of materials control, it seems my buttons are not working with being hosted in another control.
I'm getting this error in the output window:
System.Windows.Data Error: 40 : BindingExpression path error: 'ExportButtonCommand' property not found on 'object' ''TitleControl' (Name='')'. BindingExpression:Path=ExportButtonCommand; DataItem='TitleControl' (Name=''); target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')
Any help is very much appreciated.
BillOfMaterials.xaml Control
<UserControl x:Class="Bright_Instruments.Controls.BillOfMaterialsControl"
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:Bright_Instruments.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" xmlns:controls="clr-namespace:Bright_Instruments.Controls"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<controls:TitleControl Grid.Column="0" Badge="{Binding Badge, RelativeSource={RelativeSource AncestorType=UserControl}}" Grid.Row="0" Margin="5" Text="PARTS LIST" Icon="{iconPacks:PicolIcons Kind=ListNumbered}">
<controls:TitleControl.TitleContent>
<StackPanel Margin="0" HorizontalAlignment="Right" Orientation="Horizontal">
<Button ToolTip="Print parts list" Margin="5" HorizontalAlignment="Right" Style="{DynamicResource MahApps.Styles.Button.Chromeless}">
<Button.Content>
<iconPacks:PackIconEntypo Kind="Print"/>
</Button.Content>
</Button>
<Button Margin="5"
Command="{Binding ExportButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl}, UpdateSourceTrigger=PropertyChanged}" ToolTip="Export parts list to .csv" HorizontalAlignment="Right" Style="{DynamicResource MahApps.Styles.Button.Chromeless}">
<Button.Content>
<iconPacks:PackIconFontAwesome Kind="FileCsvSolid"/>
</Button.Content>
</Button>
<Button Margin="5" ToolTip="Export parts list to .pdf" HorizontalAlignment="Right" Style="{DynamicResource MahApps.Styles.Button.Chromeless}">
<Button.Content>
<iconPacks:PackIconFontAwesome Kind="FilePdfSolid"/>
</Button.Content>
</Button>
</StackPanel>
</controls:TitleControl.TitleContent>
</controls:TitleControl>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<Button Margin="0, 5, 5, 5"
Command="{Binding EditButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
Style="{DynamicResource MahApps.Styles.Button.Flat}"
Content="EDIT"/>
</StackPanel>
<DataGrid Grid.Row="1" AutoGenerateColumns="False" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding BillOfMaterials, RelativeSource={RelativeSource AncestorType=UserControl}, UpdateSourceTrigger=PropertyChanged}" Margin="5" IsReadOnly="True" SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ChildItem.PartNumber}" Width="Auto">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconFontAwesome Kind="WrenchSolid" Margin="5" />
<TextBlock Margin="5" Text="PART NUMBER"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Path=ChildItem.Description}" Width="*">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterialDesign Kind="Description" Margin="5" />
<TextBlock Margin="5" Text="DESCRIPTION"/>
</StackPanel>
</DataGridTextColumn.Header>
<DataGridTextColumn.ElementStyle>
<Style>
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
<Setter Property="TextBlock.TextAlignment" Value="Left"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Path=ChildItem.Location}" Width="Auto">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterialDesign Kind="LocationOn" Margin="5" />
<TextBlock Margin="5" Text="LOCATION"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Path=ChildItem.Quantity}" Width="Auto">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Numeric" Margin="5" />
<TextBlock Margin="5" Text="QUANTITY IN STOCK"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Width="Auto" Binding="{Binding Path=Quantity}">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="Numeric" Margin="5" />
<TextBlock Margin="5" Text="QUANTITY REQ"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTemplateColumn Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Margin="0" Style="{DynamicResource MahApps.Styles.Button.Flat}"
CommandParameter="{Binding }"
Command="{Binding ViewButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" Content="VIEW"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.Header>
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconTypicons Margin="5" Kind="Eye"/>
<TextBlock Text="VIEW" Margin="5"/>
</StackPanel>
</DataGridTemplateColumn.Header>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
BillOfMaterials.cs Control
public partial class BillOfMaterialsControl : UserControl
{
public BillOfMaterialsControl()
{
InitializeComponent();
}
public string Badge
{
get { return (string)this.GetValue(BadgeProperty); }
set { SetValue(BadgeProperty, value); }
}
public static readonly DependencyProperty BadgeProperty = DependencyProperty.Register(
"Badge", typeof(string), typeof(BillOfMaterialsControl), new PropertyMetadata(string.Empty));
public ICommand ViewButtonCommand
{
get { return (ICommand)GetValue(ViewButtonCommandProperty); }
set { SetValue(ViewButtonCommandProperty, value); }
}
public static readonly DependencyProperty ViewButtonCommandProperty = DependencyProperty.Register(
"ViewButtonCommand", typeof(ICommand), typeof(BillOfMaterialsControl), new UIPropertyMetadata(null));
public ICommand ExportButtonCommand
{
get { return (ICommand)GetValue(ExoportButtonCommandProperty); }
set { SetValue(ExoportButtonCommandProperty, value); }
}
public static readonly DependencyProperty ExoportButtonCommandProperty = DependencyProperty.Register(
"ExportButtonCommand", typeof(ICommand), typeof(BillOfMaterialsControl), new UIPropertyMetadata(null));
public ICommand EditButtonCommand
{
get { return (ICommand)GetValue(EditButtonCommandProperty); }
set { SetValue(EditButtonCommandProperty, value); }
}
public static readonly DependencyProperty EditButtonCommandProperty = DependencyProperty.Register(
"EditButtonCommand", typeof(ICommand), typeof(BillOfMaterialsControl), new UIPropertyMetadata(null));
public List<BillOfMaterial> BillOfMaterials
{
get { return (List<BillOfMaterial>)this.GetValue(BillOfMaterialsProperty); }
set { SetValue(BillOfMaterialsProperty, value); }
}
public static readonly DependencyProperty BillOfMaterialsProperty = DependencyProperty.Register(
"BillOfMaterials", typeof(List<BillOfMaterial>), typeof(BillOfMaterialsControl), new PropertyMetadata(null));
}
TitleControl.xaml Control
<UserControl x:Class="Bright_Instruments.Controls.TitleControl"
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:Bright_Instruments.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls">
<Grid Background="{DynamicResource MahApps.Brushes.Accent}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="0">
<ContentPresenter Margin="5" Content="{Binding Icon}"
TextBlock.Foreground="{DynamicResource MahApps.Brushes.IdealForeground}" />
<TextBlock
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
FontSize="12" VerticalAlignment="Center"
HorizontalAlignment="Left" Background="Transparent"
Foreground="{DynamicResource MahApps.Brushes.IdealForeground}"
Padding="5"/>
<Controls:Badged BadgePlacementMode="TopRight" Margin="10, 0, 0, 0" Badge="{Binding Badge}">
<Controls:Badged.Style>
<Style TargetType="Controls:Badged">
<Style.Triggers>
<Trigger Property="Badge" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</Controls:Badged.Style>
</Controls:Badged>
</StackPanel>
<ContentPresenter Content="{Binding TitleContent, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="0"/>
<Rectangle Height="2" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" VerticalAlignment="Bottom" Fill="{DynamicResource MahApps.Brushes.AccentBase}">
</Rectangle>
</Grid>
</UserControl>
TitleControl.cs Control
public partial class TitleControl : UserControl
{
public TitleControl()
{
InitializeComponent();
}
public string Text
{
get { return (string)this.GetValue(TextProperty); }
set { this.SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(TitleControl), new PropertyMetadata(string.Empty));
public string Badge
{
get { return (string)this.GetValue(BadgeProperty); }
set { this.SetValue(BadgeProperty, value); }
}
public static readonly DependencyProperty BadgeProperty = DependencyProperty.Register(
"Badge", typeof(string), typeof(TitleControl), new PropertyMetadata(string.Empty));
public PackIconBase Icon
{
get { return (PackIconBase)this.GetValue(IconProperty); }
set { this.SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
"Icon", typeof(PackIconBase), typeof(TitleControl), new PropertyMetadata(null));
public object TitleContent
{
get { return (object)GetValue(TitleContentProperty); }
set { SetValue(TitleContentProperty, value); }
}
public static readonly DependencyProperty TitleContentProperty = DependencyProperty.Register(
"TitleContent", typeof(object), typeof(TitleControl), new UIPropertyMetadata(null));
}
My view where the bill of materials control is hosted
<controls:BillOfMaterialsControl
EditButtonCommand="{Binding EditPartsListCommand}"
Badge="{Binding PartsListCount}"
ViewButtonCommand="{Binding ViewPartsListItemCommand}"
ExportButtonCommand="{Binding ExportPartsListToCsvCommand, UpdateSourceTrigger=PropertyChanged}"
BillOfMaterials="{Binding BillOfMaterials, UpdateSourceTrigger=PropertyChanged}"/>
Views ViewModel
public ICommand ExportPartsListToCsvCommand => new AsyncRelayCommand(ExportPartsListToCsv);
public async Task ExportPartsListToCsv()
{
var saveFileDialog = new SaveFileDialog();
var filter = $"CSV (*.csv) | *.csv";
saveFileDialog.Filter = filter;
saveFileDialog.DefaultExt = ".csv";
saveFileDialog.FileName = "Inventory.csv";
if (saveFileDialog.ShowDialog() == true)
{
try
{
await CsvService.Write<BillOfMaterial>(saveFileDialog.FileName, BillOfMaterials);
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
}
The BillOfMaterialsControl uses a TitleControl control that in turn contains a Button that binds the ExportButtonCommand (which is a property in BillOfMaterialsControl) as command. Here, you use an relative source binding with ancestor type UserControl in it.
Command="{Binding ExportButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl}, UpdateSourceTrigger=PropertyChanged}"
Picture the visual tree similiar to this (only with the essential parts in it).
BillOfMaterialsControl
TitleControl
Button
The relative source binding will search up the visual tree to find a control of type UserControl. Both of your controls derive from UserControl and the first one found is TitleControl, but this control does not contain a property called ExportButtonCommand and that is the binding failure that you get.
System.Windows.Data Error: 40 : BindingExpression path error: 'ExportButtonCommand' property not found on 'object' ''TitleControl' (Name='')'. BindingExpression:Path=ExportButtonCommand; DataItem='TitleControl' (Name=''); target element is 'Button' (Name=''); target property is 'Command' (type 'ICommand')
You could solve this issue in one of these ways.
Modify the binding to use the concrete type of your control BillOfMaterialsControl as ancestor type. This way TitleControl does not match.
Command="{Binding ExportButtonCommand, RelativeSource={RelativeSource AncestorType=local:BillOfMaterialsControl}, UpdateSourceTrigger=PropertyChanged}"
Specify a suitable AncestorLevel to skip controls in the search process.
Command="{Binding ExportButtonCommand, RelativeSource={RelativeSource AncestorType=UserControl, AncestorLevel=2}, UpdateSourceTrigger=PropertyChanged}"
A value of 2 should work (if not, adapt it). From the documentation on AncestorLevel:
Gets or sets the level of ancestor to look for, in FindAncestor mode. Use 1 to indicate the one nearest to the binding target element.
I have the following user control (Realy a TextBox control now):
<TextBox:Class="IM.Common.UIControls.IMTextBox"
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"
>
<Validation.ErrorTemplate>
<ControlTemplate>
<!--Show this if there is a validation error-->
<StackPanel Orientation="Horizontal" ToolTip="{Binding [0].ErrorContent}" >
<Border BorderThickness="2" BorderBrush="Orange" >
<AdornedElementPlaceholder Margin="-1" />
</Border>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
Code Behind:
namespace IM.Common.UIControls
{
public partial class IMTextBox
{
public IMTextBox()
{
InitializeComponent();
}
}
}
I have the Following Model:
public class User : IDataErrorInfo, INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
// used just to know if passwords match
public string Password2
{
get { return _password2; }
set
{
_password2 = value;
OnPropertyChanged("Password2");
}
}
private string _password2;
public string Error
{
get
{
throw new NotImplementedException();
}
}
public string this[string columnName]
{
get
{
if (columnName == "Password2")
{
if (string.IsNullOrEmpty(Password2))
return "required";
if (Regex.Match(Password2, "\\s").Success)
return "Password cannot contain spaces";
}
return null;
}
}
}
When I use that "usercontrol" as:
<myControls:IMTextBox Text="{Binding SomeUser.Password2, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />
It works amazing! Validation errors show and it works as expected.
Now here is my problem :/
I want to add a label to that user control and have validations still work. As a result the root of my usercontrol can no longer be the TextBox itself. As a result I modified the usercontrol to look like:
<UserControl:Class="IM.Common.UIControls.IMTextBox"
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"
>
<StackPanel>
<TextBlock Text="{Binding LabelTxt}" />
<TextBox Text="{Binding Txt, ValidatesOnDataErrors=true, NotifyOnValidationError=true}">
<Validation.ErrorTemplate>
<ControlTemplate>
<!--Show this if there is a validation error-->
<StackPanel Orientation="Horizontal" ToolTip="{Binding [0].ErrorContent}" >
<Border BorderThickness="2" BorderBrush="Orange" >
<AdornedElementPlaceholder Margin="-1" />
</Border>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
</StackPanel>
</UserControl>
The code behind now looks like:
namespace IM.Common.UIControls
{
public partial class IMTextBox : UserControl
{
public IMTextBox()
{
InitializeComponent();
this.DataContext = this;
}
public string Txt
{
get
{
return (string)GetValue(TxtProperty);
}
set
{
SetValue(TxtProperty, value);
}
}
public static DependencyProperty TxtProperty = DependencyProperty.Register(
name: "Txt",
propertyType: typeof(string),
ownerType: typeof(IMTextBox),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: string.Empty
)
);
}
}
Now when I try to use the usercontrol I am able to do:
<myControls:IMTextBox Txt="{Binding SomeUser.Password2, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />
But the validation error no longer fires :( . In other words if I where to enter "foo foo" the textbox will turn orange on the first example but not on the last example where the root control is a UserControl instead of a TextBox.
How can I still make validation work?
Edit
Thanks to the answer from alek kowalczyk I googled his solution because I did not understood his answer and came up with this solution:
http://dutton.me.uk/tag/xnamepart_contenthost/
Your issue is in UserControl binding.
<TextBox Text="{Binding Txt, Mode=TwoWay, NotifyOnValidationError=True, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:IMTextBox}}, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}">
and in dependency property declaration.
public static DependencyProperty TxtProperty = DependencyProperty.Register("Txt", typeof(string), typeof(IMTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, null , false, UpdateSourceTrigger.PropertyChanged)
When you're binding Txt property to TextBox.Text property - TextBox does not know the context, where it should find Txt property. You should tell that this property exists in parent element of IMTextBox type.
Also, Txt property has default binding OneWay, and will be updated on "Focus Leave". You need to override it in Metadata.
In Binding Txt to Text - tell that this binding is TwoWay and will be updated on each changing.
UPD: working example:
xaml:
<UserControl x:Class="IM.Common.UIControls.IMTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:IM.Common.UIControls">
<StackPanel>
<TextBox Name="tb" Text="{Binding Txt, Mode=TwoWay, NotifyOnValidationError=True, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:IMTextBox}}, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}" Validation.ErrorTemplate="{x:Null}">
</TextBox>
<StackPanel Orientation="Vertical">
<ItemsControl ItemsSource="{Binding Path=(Validation.Errors), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:IMTextBox}}}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type ValidationError}">
<Border BorderThickness="2" BorderBrush="Green" >
<TextBlock Text="{Binding ErrorContent}"></TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" Background="Green"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ContentPresenter></ContentPresenter>
</StackPanel>
</StackPanel>
cs:
namespace IM.Common.UIControls
{
public partial class IMTextBox : UserControl
{
public IMTextBox()
{
InitializeComponent();
}
public string Txt
{
get
{
return (string)GetValue(TxtProperty);
}
set
{
SetValue(TxtProperty, value);
}
}
public static DependencyProperty TxtProperty = DependencyProperty.Register("Txt", typeof(string), typeof(IMTextBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, null, false, UpdateSourceTrigger.PropertyChanged));
}
}
The DataContext of your UserControl is different from the one of your Window, so the validation error doesn't get to the textbox, I would suggest to do an custom control derived from TextBox instead of an user control.
Here you have a control template for a textbox with a label, you can store the control template in a resource dictionary if you want to reuse it on several textboxes:
<TextBox Text="{Binding txt}">
<TextBox.Template>
<ControlTemplate>
<StackPanel>
<TextBlock Text="{Binding labelTxt}" />
<ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
</StackPanel>
</ControlTemplate>
</TextBox.Template>
</TextBox>
There are similar questions to this one on here however I have tried the mentioned solutions to no avail!
Let me just run you through the setup - I have a model which implements IDataErrorInfo, a viewmodel which exposes the model to the view, within the view I have a usercontrol which is simply a labelled textbox, the model properties are binded to the usercontrol's inner textbox via a dependency property... and everything is binding correctly, all validation is fired and the correct errors returned! However, the usercontrol appears to be intercepting the error and thus the errortemplate of the usercontrol is displayed and not the textbox.
So, I know I can stop the usercontrol's error template from being displayed by setting the property to x:Null, however how do I trigger the textbox's error template to be displayed?! I have tried implementing IDataErrorInfo within the usercontrol (as advised by some) and explicitly defining the validation error template within the user control but I just can't get the darn thing to display. At this point I am thinking that the usercontrol is simply intercepting the error, holding onto it and not passing it onto the textbox, hence the errortemplate not being shown as it isn't aware of the error.
I have been pulling my hair out for the past day and really don't want to resort to not using the usercontrol as I know this can be achieved but I really don't know how to fix it! So if there are any wizards out there that can help I would be very grateful!
UserControl XAML:
<UserControl x:Class="PIRS_Client.Control.LabelTextBox"
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"
mc:Ignorable="d" Height="40.541" Width="321.027">
<Grid Height="41" VerticalAlignment="Top" HorizontalAlignment="Left" Width="321">
<StackPanel Orientation="Horizontal" Margin="0,8,50,9">
<Label Content="Label" Height="28" Name="BaseLabel" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="116" FontSize="11" />
<TextBox Height="22" Width="100" Margin="0,0,0,0" x:Name="BaseTextBox" VerticalContentAlignment="Center" VerticalAlignment="Top" FontSize="11"/>
</StackPanel>
</Grid>
UserControl Code:
public partial class LabelTextBox : UserControl
{
public static readonly DependencyProperty TextBoxTextProperty = DependencyProperty.Register("TextBoxText", typeof(string), typeof(LabelTextBox), new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true });
public LabelTextBox()
{
InitializeComponent();
Binding textBoxText = new Binding("TextBoxText") { Source = this, Mode = BindingMode.TwoWay };
BaseTextBox.SetBinding(TextBox.TextProperty, textBoxText);
}
[Browsable(true)]
public string LabelText
{
get { return BaseLabel.Content.ToString(); }
set
{
BaseLabel.Content = value;
}
}
[Browsable(true)]
public string TextBoxText
{
get { return (string)GetValue(TextBoxTextProperty); }
set { SetValue(TextBoxTextProperty, value); }
}
[Browsable(true)]
public double TextBoxWidth
{
get { return BaseTextBox.Width; }
set
{
BaseTextBox.Width = value;
}
}
}
View - UserControl delcaration:
<control:LabelTextBox HorizontalAlignment="Left" LabelText="Email" TextBoxText="{Binding UpdateSourceTrigger=LostFocus, Path=NewFosterCarerInfo.partner_email, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" TextBoxWidth="120" Margin="190,182,-61,0" VerticalAlignment="Top" Height="41" Width="321"/>
For anyone with my problem, here is the working code
UserControl xaml:
<UserControl x:Class="PIRS_Client.Control.LabelTextBox"
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"
mc:Ignorable="d" Height="40.541" Width="321.027"
x:Name="Parent" Validation.ErrorTemplate="{x:Null}">
<Grid Height="41" VerticalAlignment="Top" HorizontalAlignment="Left" Width="321" DataContext="{Binding ElementName=Parent, ValidatesOnDataErrors=True}">
<StackPanel Orientation="Horizontal" Margin="0,8,50,9">
<Label Content="Label" Height="28" Name="BaseLabel" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="116" FontSize="11" />
<TextBox Height="22" Text="{Binding Path=TextBoxText, ValidatesOnDataErrors=True}" Width="100" Margin="0,0,0,0" x:Name="BaseTextBox" VerticalContentAlignment="Center" VerticalAlignment="Top" FontSize="11"/>
</StackPanel>
</Grid>
UserControl code behind:
public partial class LabelTextBox : UserControl, IDataErrorInfo
{
public LabelTextBox()
{
InitializeComponent();
}
public static readonly DependencyProperty TextBoxTextProperty =
DependencyProperty.Register(
"TextBoxText",
typeof(string),
typeof(LabelTextBox),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
);
#region IDataErrorInfo Members
public string Error
{
get
{
if (Validation.GetHasError(this))
return string.Join(Environment.NewLine, Validation.GetErrors(this).Select(e => e.ErrorContent));
return null;
}
}
public string this[string columnName]
{
get
{
// use a specific validation or ask for UserControl Validation Error
if (Validation.GetHasError(this))
{
var error = Validation.GetErrors(this).FirstOrDefault(e => ((BindingExpression)e.BindingInError).TargetProperty.Name == columnName);
if (error != null)
return error.ErrorContent as string;
}
return null;
}
}
#endregion
[Browsable(true)]
public string LabelText
{
get { return BaseLabel.Content.ToString(); }
set { BaseLabel.Content = value; }
}
[Browsable(true)]
public string TextBoxText
{
get { return (string)GetValue(TextBoxTextProperty); }
set {
SetValue(TextBoxTextProperty, value);
}
}
[Browsable(true)]
public double TextBoxWidth
{
get { return BaseTextBox.Width; }
set { BaseTextBox.Width = value; }
}
}
Using the UserControl:
<control:LabelTextBox HorizontalAlignment="Left" LabelText="Email" TextBoxText="{Binding Path=NewFosterCarerInfo.partner_email, ValidatesOnDataErrors=true}" TextBoxWidth="120" Margin="190,182,-61,0" VerticalAlignment="Top" Height="41" Width="321"/>
And in case you wanted a nice Validation.ErrorTemplate:
`<Style TargetType="{x:Type TextBox}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,2,40,2" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="true">
<Border Background="Red" DockPanel.Dock="right" Margin="5,0,0,0" Width="20" Height="20" CornerRadius="10"
ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
<TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white">
</TextBlock>
</Border>
<AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
<Border BorderBrush="red" BorderThickness="1" />
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
I've the custom control with 3 DependencyProperty:
<UserControl x:Class="WpfDemoApp.NewsCard" [...]>
<Border BorderThickness="10" CornerRadius="10" BorderBrush="Wheat" Background="Wheat" Margin="3">
<StackPanel Background="Wheat">
<TextBlock Text="{Binding Date}" TextAlignment="Left"/>
<TextBlock Text="{Binding Title}" FontWeight="Bold" TextAlignment="Left" TextWrapping="Wrap" Margin="0, 3, 0, 3"/>
<TextBlock Text="Description:" FontWeight="Bold" />
<TextBlock Text="{Binding Text}" TextAlignment="Justify" TextWrapping="Wrap" TextTrimming="WordEllipsis"/>
</StackPanel>
</Border>
</UserControl>
In the c# code:
public partial class NewsCard : UserControl
{
public NewsCard()
{
InitializeComponent();
DataContext = this;
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(NewsCard), new UIPropertyMetadata("<title>"));
public string Date
{
get { return (string)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("Date", typeof(string), typeof(NewsCard), new UIPropertyMetadata("01.01.1970"));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(NewsCard), new UIPropertyMetadata("<text>"));
}
I'm using the following XML Data Provider:
<XmlDataProvider x:Key="RssData" XPath="//item" Source="http://www.engadget.com/rss.xml"/>
And try to use it the main window XAML file:
<demo:NewsCard Title="{Binding Source=RssData, XPath=title[1]}" Date="{Binding Source=RssData, XPath=pubDate[1]}" Text="{Binding Source=RssData, XPath=description[1]}"/>
Got the same error message for all properties:
System.Windows.Data Error: 45 : BindingExpression with XPath cannot bind to non-XML object.; XPath='title[1]' BindingExpression:Path=/InnerText; DataItem='String' (HashCode=-696447263); target element is 'NewsCard' (Name=''); target property is 'Title' (type 'String') RssData
When I use totally the same binding expression inside Text property of TextBox control everything works fine. What's wrong when I try to use it with my control? Help is very appreciated!
No idea why it would work on a TextBox, but the Binding Source should be specified as StaticResource. Otherwise the source object is just the string "RssData".
Title="{Binding Source={StaticResource RssData}, XPath=title[1]}"
Finally I found the root of "evil". In my custom component class Context was set to the class self-instance, this prevent XML data to be set as the context for the component.
That's why I got "XPath cannot bind to non-XML object", context points to the object itself and since it is not an XML data - using of XPath raise the error.
Hence I removed the following line:
DataContext = this;
from the constructor and set the relative databinding in the XAML file:
[...]
<StackPanel Background="Wheat">
<TextBlock Text="{Binding Date, RelativeSource={RelativeSource AncestorType={x:Type wpfDemoApp:NewsCard}}}" TextAlignment="Left"/>
<TextBlock Text="{Binding Title, RelativeSource={RelativeSource AncestorType={x:Type wpfDemoApp:NewsCard}}}" FontWeight="Bold" TextAlignment="Left" TextWrapping="Wrap" Margin="0, 3, 0, 3"/>
<TextBlock Text="Description:" FontWeight="Bold" />
<TextBlock Text="{Binding Text, RelativeSource={RelativeSource AncestorType={x:Type wpfDemoApp:NewsCard}}}" TextAlignment="Justify" TextWrapping="Wrap" TextTrimming="WordEllipsis"/>
</StackPanel>
after these changes the component works as expected.
So, for short:
remove explicit Context set in the constructor
set RelativeSource binding in the XAML
I've some simple code below that uses a ToggleButton.IsChecked property to set the Visibility of a TextBlock. It works fine. Since this doesn't quite fit in with my program's structure, I'm trying to bind the visibility of another TextBlock to a DependencyProperty of "this". It compiles fine, but it produces no effect. I'm doing something wrong, just not sure what.
XAML
<Window x:Class="ToggleButtonTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="200" Height="100">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<StackPanel>
<ToggleButton x:Name="toggleButton" Content="Toggle"
IsChecked="True" Checked="toggleButton_Checked"/>
<TextBlock Text="Some Text"
Visibility="{Binding IsChecked,
ElementName=toggleButton,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text="More Text"
Visibility="{Binding ShowMoreText,
ElementName=this,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Window>
C#
using System.Windows;
namespace ToggleButtonTest
{
public partial class MainWindow : Window
{
static MainWindow()
{
FrameworkPropertyMetadata meta =
new FrameworkPropertyMetadata(true,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);
ShowMoreTextProperty =
DependencyProperty.Register("ShowMoreText",
typeof(bool), typeof(MainWindow), meta);
}
public MainWindow()
{
InitializeComponent();
}
public static readonly DependencyProperty ShowMoreTextProperty;
public bool ShowMoreText
{
get
{
return (bool)GetValue(ShowMoreTextProperty);
}
set
{
SetValue(ShowMoreTextProperty, value);
}
}
private void toggleButton_Checked(object sender, RoutedEventArgs e)
{
ShowMoreText = toggleButton.IsChecked.Value;
}
}
}
Edit:
Having had this answered, I want to post my working code...
XAML
<Window x:Class="ToggleButtonTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="200" Height="100"
Name="thisWindow">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<StackPanel>
<ToggleButton x:Name="toggleButton"
Content="Toggle"
IsChecked="{Binding Path=ShowMoreText, ElementName=thisWindow}"/>
<TextBlock Text="More Text"
Visibility="{Binding Path=ShowMoreText,
ElementName=thisWindow,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Window>
C#
using System.Windows;
namespace ToggleButtonTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public static readonly DependencyProperty ShowMoreTextProperty =
DependencyProperty.Register("ShowMoreText", typeof(bool),
typeof(MainWindow), new FrameworkPropertyMetadata(true,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public bool ShowMoreText
{
get
{
return (bool)GetValue(ShowMoreTextProperty);
}
set
{
SetValue(ShowMoreTextProperty, value);
}
}
}
}
ElementName must really be an element name. this doesn't fly. Fortunately, you do have an element of type MainWindow here with a ShowMoreText property: the root Window element.
Give the Window a name and use that as ElementName, as below:
<Window x:Class="ToggleButtonTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="200" Height="100"
x:Name="thisWindow">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<StackPanel>
<ToggleButton x:Name="toggleButton" Content="Toggle"
IsChecked="True" Checked="toggleButton_Checked"/>
<TextBlock Text="Some Text"
Visibility="{Binding IsChecked,
ElementName=toggleButton,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text="More Text"
Visibility="{Binding ShowMoreText,
ElementName=thisWindow,
Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Window>
Note that you can do the same using RelativeSource Self, but I prefer the method above.
The way you have it set up currently won't ever set ShowMoreText to false. The Checked handler will only be called when the ToggleButton's IsChecked changes from false to true. To also go the other way you need a handler for Unchecked as well. The best way to handle this situation would be to instead set a Binding on the ToggleButton that will do both without any event handlers (using Jay's changes):
IsChecked="{Binding Path=ShowMoreText, ElementName=thisWindow}"
Give your Window a name and set the ElementName to that name, instead of using "this".