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>
Related
Have style for a series of buttons btn1, btn2, btn3, etc.
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
Now, I would like for the TextBlock name to be tied to the button name. For example - btn1's text block's name would be btn1Txt. The purpose of this would be the end user can assign each button its own text in a settings menu.
Any hints on how I would go about this? I admit I'm relatively new to WPF and bindings.
EDIT:::: WHAT I"VE GOT SO FAR THAT IS WORKING.
On load, the program checks the settings file for the Text for each button. Each button's content is assigned the proper information. Then inside the style, I bind the TextBlock Text to the content of the parent button.
This may not be the normal way of going about it, but it works
Method
List<string> MainButtons = Properties.Settings.Default.MainButtonNames.Cast<string>().ToList();
for (int i = 0; i < MainButtons.Count(); i++)
{
string actualNum = Convert.ToString((i + 1));
var MainButtonFinder = (Button)this.FindName("MainButton" + actualNum);
Console.WriteLine(MainButtonFinder.Name);
MainButtonFinder.Content = MainButtons[i];
Console.WriteLine(MainButtonFinder.Content);
}
Style
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="100px"/>
<Setter Property="MinHeight" Value="50"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="20" Height="45" Width="100" Margin="0" Background="#FF99CCFF">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="LCARS" Foreground="White" Padding="5px" FontSize="18px" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
This is the wrong way to go about what you're trying to do. Here's the "right" way to do it. There's a fair amount of boilerplate code here, but you get used to it.
Write a button viewmodel and give your main viewmodel an ObservableCollection of those:
#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 MainViewModel Class
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
ButtonItems.Add(new ButtonItemViewModel("First Command", "First Item", () => MessageBox.Show("First Item Executed")));
ButtonItems.Add(new ButtonItemViewModel("Second Command", "Second Item", () => MessageBox.Show("Second Item Executed")));
}
#region ButtonItems Property
public ObservableCollection<ButtonItemViewModel> ButtonItems { get; }
= new ObservableCollection<ButtonItemViewModel>();
#endregion ButtonItems Property
}
#endregion MainViewModel Class
#region ButtonItemViewModel Class
public class ButtonItemViewModel : ViewModelBase
{
public ButtonItemViewModel(String cmdName, String text, Action cmdAction)
{
CommandName = cmdName;
Text = text;
Command = new DelegateCommand(cmdAction);
}
#region Text Property
private String _text = default(String);
public String Text
{
get { return _text; }
set
{
if (value != _text)
{
_text = value;
OnPropertyChanged();
}
}
}
#endregion Text Property
#region CommandName Property
private String _commandName = default(String);
public String CommandName
{
get { return _commandName; }
private set
{
if (value != _commandName)
{
_commandName = value;
OnPropertyChanged();
}
}
}
#endregion CommandName Property
public ICommand Command { get; private set; }
}
#endregion ButtonItemViewModel Class
public class DelegateCommand : ICommand
{
public DelegateCommand(Action action)
{
_action = action;
}
private Action _action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action?.Invoke();
}
}
Make that MainViewModel the DataContext of your Window:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here's how you can put it all together in the XAML:
<Grid>
<StackPanel Orientation="Vertical">
<GroupBox Header="Buttons">
<ItemsControl ItemsSource="{Binding ButtonItems}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2"
MinWidth="80"
Content="{Binding Text}"
Command="{Binding Command}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Edit Buttons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Margin="2"
x:Name="ButtonEditorListBox"
ItemsSource="{Binding ButtonItems}"
HorizontalAlignment="Left"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2"
Text="{Binding CommandName}"
/>
<TextBlock
Margin="2"
Text="{Binding Text, StringFormat=': "{0}"'}"
/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<ContentControl
Grid.Column="1"
Margin="8,2,2,2"
Content="{Binding SelectedItem, ElementName=ButtonEditorListBox}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock
Margin="2"
HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding CommandName, StringFormat={}{0}: }"
/>
<TextBox
Margin="2"
HorizontalAlignment="Stretch"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
Back to your question:
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
You're doing styles wrong. Very, very wrong. I can help you fix it if you show me the style.
As I understand what you want is to modify the "text" of the button, it occurs to me that you can do it this way.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=ButtonTest, Path=Content, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
<Button Name="ButtonTest" Grid.Row="1" Width="100" Height="40">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" Text="{Binding}"></TextBlock>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Grid>
I have a problem with bindings for DataTemplate based on defined DataType in ItemsControl, when I want to bind to my custom user control.
For demonstration purposes, I've created simple Item class example, where I have collection of items like this:
public class Item
{
public string ItemNameToBeSureWhatPropertyIsBound { get; set; }
}
In my ViewModel I create such collection, and expose it (with one item for comparison separately):
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<Item> _items;
private Item _exampleItem;
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new[] { new Item { ItemNameToBeSureWhatPropertyIsBound = "Me" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "MySelf" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "Ich" }, });
ExampleItem = Items.LastOrDefault();
}
public ObservableCollection<Item> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
public Item ExampleItem
{
get { return _exampleItem; }
set { _exampleItem = value; OnPropertyChanged();}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
My custom user control is defined like this:
<UserControl x:Class="WpfDataTemplate.ItemRowUserControl"
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="40" d:DesignWidth="300"
x:Name="ItemRowControl" DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}">
<Grid Background="Yellow" Height="40">
<TextBlock Text="{Binding ItemName}" Foreground="Black"/>
</Grid>
</UserControl>
...and it has one DependencyProperty in code behind:
public partial class ItemRowUserControl : UserControl
{
public ItemRowUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ItemNameProperty = DependencyProperty.Register(
"ItemName", typeof (string), typeof (ItemRowUserControl), new PropertyMetadata(default(string)));
public string ItemName
{
get { return (string) GetValue(ItemNameProperty); }
set { SetValue(ItemNameProperty, value); }
}
}
The problem is, when I try to bind to property of Item in DataTemplate for ItemsControl, which I'm doing in MainWindow like this (note: I have dummy converter for debugging purposes only, returning value back, and nothing more):
<Window.DataContext>
<my:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<my:MyDummyConverter x:Key="MyDummyConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<ItemsControl Name="ItemsControl" ItemsSource="{Binding Items}" Grid.Row="0" Background="Red">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type my:Item}">
<my:ItemRowUserControl ItemName="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" />
<!--<Grid Background="Pink">
<TextBlock Text="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" Foreground="Black" Height="30" />
</Grid>-->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Grid.Row="1">
<my:ItemRowUserControl ItemName="{Binding DataContext.ExampleItem.ItemNameToBeSureWhatPropertyIsBound, ElementName=MyWindow, Converter={StaticResource MyDummyConverter}}" />
</Grid>
</Grid>
Now, in case I bind to my custom ItemRowUserControl, the value I get into converter (and I see the same in Debug Output) is ItemRowUserControl itself. But if I bind to commented out code, everything works fine. Why is that, and how can I have custom control for DataTemplate so that bindings (offered by intellisense) will work? On the side note: binding to my ItemRowUserControl in grid row 1 (at the bottom) works fine, so I guess control is set to work as expected?
The problem is that you explicitly set the DataContext of your UserControl to itself:
DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}
Remove that assignment and write the ItemName binding like this:
<TextBlock Text="{Binding ItemName,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
or like this
<TextBlock Text="{Binding ItemName, ElementName=ItemRowControl}"/>
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>
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>`
My problem:
I have a listbox with owners of dogs, and i have a listbox with dogs. I want to modify the dogs listbox itemtemplate as the following: DogName(textblock)+DogKind(textblock)+Owners(combobox).The first two was successful, but i cant add the existing owners to the combobox. If i give a name to my combobox like :
<ComboBox x:Name="mycombo" />
i cant see the mycombo variable in the c# code.
The XAML:
<Window x:Class="CodeFirst.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sajat="clr-namespace:CodeFirst"
Title="MainWindow" Height="557.638" Width="721.294"
>
<Grid x:Name="grid1">
<ListBox x:Name="listbox2" HorizontalAlignment="Left" Height="313" Margin="338,10,0,0" VerticalAlignment="Top" Width="250">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text=", "/>
<TextBlock Text="{Binding Path=Kind}"/>
<ComboBox />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
How can i give the itemsource to the combobox, or how can i reach to add the owners?
If you use the DataContext, you can set the Binding like this:
<ComboBox ItemsSource="{Binding Path=DataContext.MyItemsSource, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"></ComboBox>
First of all, in order to work with WPF or other XAML-based technologies, you must understand that
UI is not Data. Data is Data. UI is UI.
This means that you should not manipulate any ComboBox or any other UI elements in code, in order to populate them with data, but instead create a ViewModel and bind these objects to that.
In this example, the Window itself is used as ViewModel because it's a simple example, but you should consider moving all application logic to a separate class:
<Window x:Class="MiscSamples.UIisNotData"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="UIisNotData" Height="300" Width="300">
<UniformGrid Rows="1" Columns="2">
<DockPanel>
<TextBlock Text="Owners:" DockPanel.Dock="Top" FontWeight="Bold" TextAlignment="Center" Margin="2"/>
<Button Content="Add" Width="80" DockPanel.Dock="Bottom" Margin="2" Click="AddOwner"/>
<ListBox ItemsSource="{Binding Owners}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Name}" x:Name="block"/>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Visibility="Collapsed" x:Name="box"/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType=ListBoxItem}}" Value="True">
<Setter TargetName="block" Property="Visibility" Value="Collapsed"/>
<Setter TargetName="box" Property="Visibility" Value="Visible"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<DockPanel>
<TextBlock Text="Dogs:" DockPanel.Dock="Top" FontWeight="Bold" TextAlignment="Center" Margin="2"/>
<ListBox ItemsSource="{Binding Dogs}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<ComboBox ItemsSource="{Binding DataContext.Owners, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
SelectedItem="{Binding Owner}" DisplayMemberPath="Name"
DockPanel.Dock="Right" Width="100"/>
<TextBlock>
<Run Text="{Binding Name}"/>
<Run Text=", "/>
<Run Text="{Binding Kind}"/>
</TextBlock>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UniformGrid>
</Window>
Code Behind (This code should be placed in a ViewModel):
public partial class UIisNotData : Window
{
public ObservableCollection<Owner> Owners { get; set; }
public ObservableCollection<string> Kinds { get; set; }
public ObservableCollection<Dog> Dogs { get; set; }
public UIisNotData()
{
InitializeComponent();
Owners = new ObservableCollection<Owner>
{
new Owner() {Name = "Jack"},
new Owner() {Name = "Mike"},
new Owner() {Name = "Kirk"},
new Owner() {Name = "John"},
};
Kinds = new ObservableCollection<string>
{
"Affenpinscher",
"Afghan Hound",
"Airedale Terrier",
"Akita"
//.. All the rest of dog Breeds taken from http://www.petmd.com/dog/breeds?breed_list=az#.UVsQKpPcmQo
};
Dogs = new ObservableCollection<Dog>
{
new Dog() {Name = "Bobby", Kind = Kinds[0], Owner = Owners[0]},
new Dog() {Name = "Fido", Kind = Kinds[1], Owner = Owners[1]},
new Dog() {Name = "Toby", Kind = Kinds[2], Owner = Owners[2]}
};
DataContext = this;
}
private void AddOwner(object sender, RoutedEventArgs e)
{
Owners.Add(new Owner(){Name = "New Owner"});
}
}
Data Model:
public class Owner : PropertyChangedBase
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public class Dog: PropertyChangedBase
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
private Owner _owner;
public Owner Owner
{
get { return _owner; }
set
{
_owner = value;
OnPropertyChanged("Owner");
}
}
private string _kind;
public string Kind
{
get { return _kind; }
set
{
_kind = value;
OnPropertyChanged("Kind");
}
}
}
PropertyChangedBase Class:
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Result:
There are 3 important aspects you need to consider about this example:
I am in no way manipulating UI elements in code. That's completely unnecessary most of the time in WPF.
The classes from the Data Model implement INotifyPropertyChanged in order to support 2-way binding in WPF.
The Collections are of type ObservableCollection<T> in order to support automatic notification when elements are added/removed from the collection (in order to automatically update the ListBoxes, etc).
Another thing you may notice is that the XAML elements in my example have no specific size or Margin values. Things like Margin="338,10,0,0" is usually what you get from the Visual Studio designer and indicates a poorly structured layout. I recommend you look at the Layout elements in WPF (DockPanel, StackPanel, Grid, UniformGrid, WrapPanel, etc), and start coding the XAML yourself instead of using the designer. This will allow a much higher level of scalability and will also save you from the nuances of Fixed-position elements.