How to display the error message in WPF app? - c#

I am trying to create an application using WPF. I am trying to fully build it using the MVVM model. However, I am puzzled on how to correctly display the error message. I thought it would be trivial step but seems to be the most complex.
I created the following view using xaml
<StackPanel Style="{StaticResource Col}">
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" ></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Style="{StaticResource Col}">
<Label Content="Name" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Name" Style="{StaticResource FormControl}" Text="{Binding Name, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Style="{StaticResource Col}">
<Label Content="Phone Number" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Phone" Style="{StaticResource FormControl}" Text="{Binding Phone, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Style="{StaticResource PrimaryButton}" Command="{Binding Create}">Create</Button>
<Button>Reset</Button>
</StackPanel>
</DockPanel>
</StackPanel>
Then I created the following ViewModel
public class VendorViewModel : ViewModel
{
protected readonly IUnitOfWork UnitOfWork;
private string _Name { get; set; }
private string _Phone { get; set; }
public VendorViewModel()
: this(new UnitOfWork())
{
}
public VendorViewModel(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
}
[Required(ErrorMessage = "The name is required")]
[MinLength(5, ErrorMessage = "Name must be more than or equal to 5 letters")]
[MaxLength(50, ErrorMessage = "Name must be less than or equal to 50 letters")]
public string Name
{
get { return _Name; }
set
{
_Name = value;
NotifyPropertyChanged();
}
}
public string Phone
{
get { return _Phone; }
set
{
_Phone = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// Gets the collection of customer loaded from the data store.
/// </summary>
public ICollection<Vendor> Vendors { get; private set; }
protected void AddVendor()
{
var vendor = new Vendor(Name, Phone);
UnitOfWork.Vendors.Add(vendor);
}
public ICommand Create
{
get
{
return new ActionCommand(p => AddVendor(),
p => IsValidRequest());
}
}
public bool IsValidRequest()
{
// There got to be a better way to check if everything passed or now...
return IsValid("Name") && IsValid("Phone");
}
}
Here is how my ViewModel base class look like
public abstract class ViewModel : ObservableObject, IDataErrorInfo
{
/// <summary>
/// Gets the validation error for a property whose name matches the specified <see cref="columnName"/>.
/// </summary>
/// <param name="columnName">The name of the property to validate.</param>
/// <returns>Returns a validation error if there is one, otherwise returns null.</returns>
public string this[string columnName]
{
get { return OnValidate(columnName); }
}
/// <summary>
/// Validates a property whose name matches the specified <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">The name of the property to validate.</param>
/// <returns>Returns a validation error, if any, otherwise returns null.</returns>
protected virtual string OnValidate(string propertyName)
{
var context = new ValidationContext(this)
{
MemberName = propertyName
};
var results = new Collection<ValidationResult>();
bool isValid = Validator.TryValidateObject(this, context, results, true);
if (!isValid)
{
ValidationResult result = results.SingleOrDefault(p => p.MemberNames.Any(memberName => memberName == propertyName));
if (result != null)
return result.ErrorMessage;
}
return null;
}
protected virtual bool IsValid(string propertyName)
{
return OnValidate(propertyName) == null;
}
/// <summary>
/// Not supported.
/// </summary>
[Obsolete]
public string Error
{
get
{
throw new NotSupportedException();
}
}
}
Here is my ObservableObject class
public class ObservableObject : INotifyPropertyChanged
{
/// <summary>
/// Raised when the value of a property has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises <see cref="PropertyChanged"/> for the property whose name matches <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">Optional. The name of the property whose value has changed.</param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
My goal is to show a red border around the incorrect field then display the error message right underneath it to tell the use what went wrong.
How do I show the error correctly? Also, how to I not show any error when the view is first loaded?
Base on this blog I need to edit the Validation.ErrorTemplate
So I tried adding the following code to the App.xaml file
<!-- Style the error validation by showing the text message under the field -->
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border BorderThickness="1" BorderBrush="DarkRed">
<StackPanel>
<AdornedElementPlaceholder x:Name="errorControl" />
</StackPanel>
</Border>
<TextBlock Text="{Binding AdornedElement.ToolTip, ElementName=errorControl}" Foreground="Red" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
But that isn't showing the error message, also when the view is first loaded I get an error. Finally, even when the form become valid, the action button stay disabled.
UPDATED
After moving the Property="Validation.ErrorTemplate" into the FormControl group it worked. However, the error message seems to be going over the buttons instead of pushing the buttons down. Also, the text does not seems to be wrapping vertically allowing the border to strach over the other control as you can see in the following screen shows.

I'll try to answer all your questions:
How do I show the error correctly?
The ErrorTemplate is not applied because the FormControl style on your TextBox has precedence over the style containing the Validation.ErrorTemplate. Moving the Validation.ErrorTemplate code into the FormControl style will fix this issue.
Also, how to I not show any error when the view is first loaded?
What is the use of a Required validation if it's not applied immediately? The MinLength and MaxLength validations will only be executed when you start typing in the field.
However, the error message seems to be going over the buttons instead of pushing the buttons down.
As Will pointed out, this is because the error messages are shown on the AdornerLayer, which cannot interfere with the layer your controls are on. You have the following options:
Use the AdornerLayer but leave some room between controls
Use a ToolTip to show the error messages
Use an extra TextBlock in the template of the TextBox that shows the error messages.
These options are described here

Related

CheckBox's in DataTemplate ListBox Binding and Triggers using MVVM

I'm learning how to use MVVM and how bind data inside a WPF App. I've created a custom CheckedListBox in XAML file this way:
<ListBox x:Name="materialsListBox" ItemsSource="{Binding CustomCheckBox}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsChecked, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" Content="{Binding Item}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
and also I want a single Image to dynamically show up for each CheckBox I check. I understand that I need to use Binding and UpdateSourceTrigger Property but I'm not sure how to realize this.
What should I add here so that my app does what I want?
<Image HorizontalAlignment="Left" Height="100" Margin="432,146,0,0" VerticalAlignment="Top" Width="100"/>
Here's a part of my C# code for the ViewModel:
public class MainViewModel : ViewModelBase
{
private ObservableCollection<CheckedListItem<string>> _customCheckBox;
public ObservableCollection<CheckedListItem<string>> CustomCheckBox
{
set
{
_customCheckBox = value;
OnPropertyChanged();
}
get { return _customCheckBox; }
}
public class CheckedListItem<T> : ViewModelBase
{
private bool _isChecked;
private T _item;
public CheckedListItem()
{
}
public CheckedListItem(T item, bool isChecked = false)
{
item = _item;
isChecked = _isChecked;
}
public T Item
{
set
{
_item = value;
OnPropertyChanged();
}
get { return _item; }
}
public bool IsChecked
{
set
{
_isChecked = value;
OnPropertyChanged();
}
get { return _isChecked; }
}
}
...
Thank you for any recommendation.
One eazy way to do ProperyChanged events is to use the base set for ViewModelBase this.Set because it will raise the changed event for you.
to do this I split up the view model and view in to 2, one for the main view and one for a view combining the check box and image. You can do it with one like you have but it was just easier for me.
View Model for the CheckBox and image
public class CheckBoxViewModel : ViewModelBase
{
private bool isChecked;
private string imageSource;
private string imageName;
public CheckBoxViewModel(string imageSource, string imageName)
{
this.ImageSource = imageSource;
this.ImageName = imageName;
}
public ICommand Checked => new RelayCommand<string>(this.OnChecked);
private void OnChecked(object imageName)
{
}
public string ImageSource
{
get { return this.imageSource; }
set { this.Set(() => this.ImageSource, ref this.imageSource, value); }
}
public string ImageName
{
get { return this.imageName; }
set { this.Set(() => this.ImageName, ref this.imageName, value); }
}
public bool IsChecked
{
get { return this.isChecked; }
set { this.Set(() => this.IsChecked, ref this.isChecked, value); }
}
}
Main Window View Model
public class MainViewModel : ViewModelBase
{
private ObservableCollection<CheckBoxViewModel> items = new ObservableCollection<CheckBoxViewModel>();
public ObservableCollection<CheckBoxViewModel> Items => this.items;
public MainViewModel()
{
var view = new CheckBoxViewModel("Image.Jpg", "Image 1");
this.Items.Add(view);
var view2 = new CheckBoxViewModel("Image2.Jpg", "Image 2");
this.Items.Add(view2);
}
}
Checkbox and image view
<UserControl.Resources>
<local:MainViewModel x:Key="MainViewModel" />
<local:MainViewModel x:Key="ViewModel" />
<local:BoolToVisibility x:Key="BoolToVisibility" />
</UserControl.Resources>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="201*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Command="{Binding Checked}" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding Path=IsChecked, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" Content="{Binding ImageName}" />
<Image Grid.Column="1" Source="{Binding ImageSource}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibility}}" />
</Grid>
Main View
<Window.Resources>
<local:MainViewModel x:Key="MainViewModel" />
<DataTemplate DataType="{x:Type local:CheckBoxViewModel}">
<local:view/>
</DataTemplate>
</Window.Resources>
<Grid>
<ListView DataContext="{StaticResource MainViewModel}" ItemsSource="{Binding Items}"/>
</Grid>
This way the main view model adds CheckBoxViewModels to its items and then the main view automatically adds the child view to the list view.
Whats notable is how the images visibility is flipped. I used a value converter that you add to the Images visibility Binding. It will convert a true false value to a type of Visibility.
public class BoolToVisibility : IValueConverter
{
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
if ((bool)value)
{
return Visibility.Visible;
}
else
{
return Visibility.Collapsed;
}
}
return Visibility.Collapsed;
}
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value that is produced by the binding target.</param>
/// <param name="targetType">The type to convert to.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}

Update Properties on Custom Control click

I'm working with WPF (MVVM pattern in particular) and I'm trying to create a simple application that shows a list of tasks. I created a custom control called TaskListControl that shows a list of other custom controls named TaskListItemControl and each of them has its own ViewModel.
Here is the TaskListItemControl template, where you can see the InputBindings and the Triggers that affects the control appearence when the IsSelected is set to true:
<UserControl x:Class="CSB.Tasks.TaskListItemControl"
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:CSB.Tasks"
mc:Ignorable="d"
d:DesignHeight="70"
d:DesignWidth="400">
<!-- Custom control that represents a Task. -->
<UserControl.Resources>
<!-- The control style. -->
<Style x:Key="ContentStyle" TargetType="{x:Type ContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<Border x:Name="ContainerBorder" BorderBrush="{StaticResource LightVoidnessBrush}"
Background="{StaticResource VoidnessBrush}"
BorderThickness="1"
Margin="2">
<Border.InputBindings>
<MouseBinding MouseAction="LeftClick" Command="{Binding SelctTaskCommand}"/>
</Border.InputBindings>
<!-- The grid that contains the control. -->
<Grid Name="ContainerGrid" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Border representing the priority state of the Task:
The color is defined by a ValueConverter according to the PriorityLevel of the Task object. -->
<Border Grid.Column="0"
Width="10"
Background="{Binding Priority, Converter={local:PriorityLevelToRGBConverter}}">
</Border>
<!-- Border containing the Task's informations. -->
<Border Grid.Column="1" Padding="5">
<StackPanel>
<!-- The title of the Task. -->
<TextBlock Text="{Binding Title}" FontSize="{StaticResource TaskListItemTitleFontSize}" Foreground="{StaticResource DirtyWhiteBrush}"/>
<!-- The customer the Taks refers to. -->
<TextBlock Text="{Binding Customer}" Style="{StaticResource TaskListItemControlCustomerTextBlockStyle}"/>
<!-- The description of the Task. -->
<TextBlock Text="{Binding Description}"
TextTrimming="WordEllipsis"
Foreground="{StaticResource DirtyWhiteBrush}"/>
</StackPanel>
</Border>
<!-- Border that contains the controls for the Task management. -->
<Border Grid.Column="2"
Padding="5">
<!-- Selection checkbox of the Task. -->
<CheckBox Grid.Column="2" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
<!-- Template triggers. -->
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" TargetName="ContainerBorder" Value="{StaticResource OverlayVoidnessBrush}"/>
<Setter Property="BorderBrush" TargetName="ContainerBorder" Value="{StaticResource PeterriverBrush}"/>
</DataTrigger>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Duration="0:0:0:0" To="{StaticResource OverlayVoidness}" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Duration="0:0:0:0" To="Transparent" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- Content of the control: assignment of the DataContext for design-time testing. -->
<ContentControl d:DataContext="{x:Static local:TaskListItemDesignModel.Instance}"
Style="{StaticResource ContentStyle}"/>
And here is the TaskListItemViewModel where the Action should be executed (all the PropertyChanged boilerplate code is handled inside the BaseViewModel class):
/// <summary>
/// The ViewModel for the <see cref="TaskListItemControl"/>.
/// </summary>
public class TaskListItemViewModel : BaseViewModel
{
#region Public Properties
/// <summary>
/// Priority level of the task.
/// </summary>
public PriorityLevel Priority { get; set; }
/// <summary>
/// The name of the task.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The customer the task refers to.
/// </summary>
public string Customer { get; set; }
/// <summary>
/// The description of the task.
/// </summary>
public string Description { get; set; }
/// <summary>
/// True if the Task is the selected one in the task list.
/// </summary>
public bool IsSelected { get; set; }
#endregion
#region Commands
/// <summary>
/// The command fired whenever a task is selected.
/// </summary>
public ICommand SelectTaskCommand { get; set; }
#endregion
#region Constructor
/// <summary>
/// The <see cref="TaskListItemViewModel"/> default constructor.
/// </summary>
public TaskListItemViewModel()
{
// Initialize commands.
// When the task is selected, IsSelected becomes true.
// The command will do other stuff in the future.
SelectTaskCommand = new RelayCommand(() => IsSelected = true);
}
#endregion
}
The data is provided through a design model bound to the TaskListControl control where the properties of each item of the list are hardcoded (this design model will be replaced with a database since this class just provides dummy data):
/// <summary>
/// The <see cref="TaskListControl"/> design model that provides dummy data for the XAML testing.
/// </summary>
public class TaskListDesignModel : TaskListViewModel
{
#region Public Properties
/// <summary>
/// A single instance of the <see cref="TaskListDesignModel"/> class.
/// </summary>
public static TaskListDesignModel Instance => new TaskListDesignModel();
#endregion
#region Constructor
/// <summary>
/// The <see cref="TaskListDesignModel"/> default constructor that provides dummy data.
/// </summary>
public TaskListDesignModel()
{
Items = new ObservableCollection<TaskListItemViewModel>
{
new TaskListItemViewModel
{
Title = "Activity #1",
Customer = "Internal",
Description = "This is activity #1.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #2",
Customer = "Internal",
Description = "This is activity #2.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #3",
Customer = "Internal",
Description = "This is activity #3.",
Priority = PriorityLevel.High,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #4",
Customer = "Internal",
Description = "This is activity #4.",
Priority = PriorityLevel.Medium,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #5",
Customer = "Internal",
Description = "This is activity #5.",
Priority = PriorityLevel.Medium,
IsSelected = false
},
new TaskListItemViewModel
{
Title = "Activity #6",
Customer = "Internal",
Description = "This is activity #6.",
Priority = PriorityLevel.Low,
IsSelected = false
}
};
}
#endregion
}
Here is the result:
What I want to do when the list item is selected is to update his IsSelected property in the ViewModel and change its appearence through Triggers, but nothing happens when I click on an item.
Here is the link to the GitHub repository of the entire project if needed.
What am I missing? Thank you in advance for the help.
You should fix this two problems to solve the selection issue:
1) I spotted a typo inside your TaskListItemControl:
Line 25 should be "SelectTaskCommand" on the Command binding.
This will finally invoke the command.
2) Then in your TaskListItemViewModel you have to make your properties raise the PropertyChanged event. I highly recommend this for all your ViewModel properties. But to solve your problem this must apply at least to the IsSelected property:
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
if (value.Equals(this.isSelected)
{
return;
}
this.isSelected = value;
OnPropertyChanged();
}
}
This will make any changes to IsSelected propagate and thus the DataTrigger can work as expected.
And just another recommendation is to modify the PropertyChanged invocator signature in BaseViewModel to:
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
This way you avoid to always pass the property name as an argument.
If you want the CheckBox to reflect the selection state and should be used to undo the selection, just add a TwoWay binding to its IsChecked property:
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" />

how to get selected radiobutton in viewmodel from stackpanel [duplicate]

EDIT: Problem was fixed in .NET 4.0.
I have been trying to bind a group of radio buttons to a view model using the IsChecked button. After reviewing other posts, it appears that the IsChecked property simply doesn't work. I have put together a short demo that reproduces the problem, which I have included below.
Here is my question: Is there a straightforward and reliable way to bind radio buttons using MVVM? Thanks.
Additional information: The IsChecked property doesn't work for two reasons:
When a button is selected, the IsChecked properties of other buttons in the group don't get set to false.
When a button is selected, its own IsChecked property does not get set after the first time the button is selected. I am guessing that the binding is getting trashed by WPF on the first click.
Demo project: Here is the code and markup for a simple demo that reproduces the problem. Create a WPF project and replace the markup in Window1.xaml with the following:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
<StackPanel>
<RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" />
<RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" />
</StackPanel>
</Window>
Replace the code in Window1.xaml.cs with the following code (a hack), which sets the view model:
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = new Window1ViewModel();
}
}
}
Now add the following code to the project as Window1ViewModel.cs:
using System.Windows;
namespace WpfApplication1
{
public class Window1ViewModel
{
private bool p_ButtonAIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonAIsChecked
{
get { return p_ButtonAIsChecked; }
set
{
p_ButtonAIsChecked = value;
MessageBox.Show(string.Format("Button A is checked: {0}", value));
}
}
private bool p_ButtonBIsChecked;
/// <summary>
/// Summary
/// </summary>
public bool ButtonBIsChecked
{
get { return p_ButtonBIsChecked; }
set
{
p_ButtonBIsChecked = value;
MessageBox.Show(string.Format("Button B is checked: {0}", value));
}
}
}
}
To reproduce the problem, run the app and click Button A. A message box will appear, saying that Button A's IsChecked property has been set to true. Now select Button B. Another message box will appear, saying that Button B's IsChecked property has been set to true, but there is no message box indicating that Button A's IsChecked property has been set to false--the property hasn't been changed.
Now click Button A again. The button will be selected in the window, but no message box will appear--the IsChecked property has not been changed. Finally, click on Button B again--same result. The IsChecked property is not updated at all for either button after the button is first clicked.
If you start with Jason's suggestion then the problem becomes a single bound selection from a list which translates very nicely to a ListBox. At that point it's trivial to apply styling to a ListBox control so that it shows up as a RadioButton list.
<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<RadioButton Content="{TemplateBinding Content}"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Looks like they fixed binding to the IsChecked property in .NET 4. A project that was broken in VS2008 works in VS2010.
For the benefit of anyone researching this question down the road, here is the solution I ultimately implemented. It builds on John Bowen's answer, which I selected as the best solution to the problem.
First, I created a style for a transparent list box containing radio buttons as items. Then, I created the buttons to go in the list box--my buttons are fixed, rather than read into the app as data, so I hard-coded them into the markup.
I use an enum called ListButtons in the view model to represent the buttons in the list box, and I use each button's Tag property to pass a string value of the enum value to use for that button. The ListBox.SelectedValuePath property allows me to specify the Tag property as the source for the selected value, which I bind to the view model using the SelectedValue property. I thought I would need a value converter to convert between the string and its enum value, but WPF's built-in converters handled the conversion without problem.
Here is the complete markup for Window1.xaml:
<Window x:Class="RadioButtonMvvmDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<!-- Resources -->
<Window.Resources>
<Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" >
<Setter Property="Margin" Value="5" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border BorderThickness="0" Background="Transparent">
<RadioButton
Focusable="False"
IsHitTestVisible="False"
IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter />
</RadioButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border BorderThickness="0" Padding="0" BorderBrush="Transparent" Background="Transparent" Name="Bd" SnapsToDevicePixels="True">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<!-- Layout -->
<Grid>
<!-- Note that we use SelectedValue, instead of SelectedItem. This allows us
to specify the property to take the value from, using SelectedValuePath. -->
<ListBox Style="{StaticResource RadioButtonList}" SelectedValuePath="Tag" SelectedValue="{Binding Path=SelectedButton}">
<ListBoxItem Tag="ButtonA">Button A</ListBoxItem>
<ListBoxItem Tag="ButtonB">Button B</ListBoxItem>
</ListBox>
</Grid>
</Window>
The view model has a single property, SelectedButton, which uses a ListButtons enum to show which button is selected. The property calls an event in the base class I use for view models, which raises the PropertyChanged event:
namespace RadioButtonMvvmDemo
{
public enum ListButtons {ButtonA, ButtonB}
public class Window1ViewModel : ViewModelBase
{
private ListButtons p_SelectedButton;
public Window1ViewModel()
{
SelectedButton = ListButtons.ButtonB;
}
/// <summary>
/// The button selected by the user.
/// </summary>
public ListButtons SelectedButton
{
get { return p_SelectedButton; }
set
{
p_SelectedButton = value;
base.RaisePropertyChangedEvent("SelectedButton");
}
}
}
}
In my production app, the SelectedButton setter will call a service class method that will take the action required when a button is selected.
And to be complete, here is the base class:
using System.ComponentModel;
namespace RadioButtonMvvmDemo
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Protected Methods
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the changed property.</param>
protected void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
}
#endregion
}
}
Hope that helps!
One solution is to update the ViewModel for the radio buttons in the setter of the properties. When Button A is set to True, set Button B to false.
Another important factor when binding to an object in the DataContext is that the object should implement INotifyPropertyChanged. When any bound property changes, the event should be fired and include the name of the changed property. (Null check omitted in the sample for brevity.)
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool _ButtonAChecked = true;
public bool ButtonAChecked
{
get { return _ButtonAChecked; }
set
{
_ButtonAChecked = value;
PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked"));
if (value) ButtonBChecked = false;
}
}
protected bool _ButtonBChecked;
public bool ButtonBChecked
{
get { return _ButtonBChecked; }
set
{
_ButtonBChecked = value;
PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked"));
if (value) ButtonAChecked = false;
}
}
}
Edit:
The issue is that when first clicking on Button B the IsChecked value changes and the binding feeds through, but Button A does not feed through its unchecked state to the ButtonAChecked property. By manually updating in code the ButtonAChecked property setter will get called the next time Button A is clicked.
Here is another way you can do it
VIEW:
<StackPanel Margin="90,328,965,389" Orientation="Horizontal">
<RadioButton Content="Mr" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Mrs" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Ms" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
<RadioButton Content="Other" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}}" GroupName="Title"/>
<TextBlock Text="{Binding SelectedTitle, Mode=TwoWay}"/>
</StackPanel>
ViewModel:
private string selectedTitle;
public string SelectedTitle
{
get { return selectedTitle; }
set
{
SetProperty(ref selectedTitle, value);
}
}
public RelayCommand TitleCommand
{
get
{
return new RelayCommand((p) =>
{
selectedTitle = (string)p;
});
}
}
Not sure about any IsChecked bugs, one possible refactor you could make to your viewmodel:the view has a number of mutually exclusive states represented by a series of RadioButtons, only one of which at any given time can be selected. In the view model, just have 1 property (e.g. an enum) which represents the possible states: stateA, stateB, etc That way you wouldn't need all the individual ButtonAIsChecked, etc
A small extension to John Bowen's answer: It doesn't work when the values don't implement ToString(). What you need instead of setting the Content of the RadioButton to a TemplateBinding, just put a ContentPresenter in it, like this:
<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}">
<ContentPresenter/>
</RadioButton>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
This way you can additionally use DisplayMemberPath or an ItemTemplate as appropriate. The RadioButton just "wraps" the items, providing the selection.
I know this is an old question and the original issue was resolved in .NET 4. and in all honesty this is slightly off topic.
In most cases where I've wanted to use RadioButtons in MVVM it's to select between elements of an enum, this requires binding a bool property in the VM space to each button and using them to set an overall enum property that reflects the actual selection, this gets very tedious very quick. So I came up with a solution that is re-usable and very easy to implement, and does not require ValueConverters.
The View is pretty much the same, but once you have your enum in place the VM side can be done with a single property.
MainWindowVM
using System.ComponentModel;
namespace EnumSelectorTest
{
public class MainWindowVM : INotifyPropertyChanged
{
public EnumSelectorVM Selector { get; set; }
private string _colorName;
public string ColorName
{
get { return _colorName; }
set
{
if (_colorName == value) return;
_colorName = value;
RaisePropertyChanged("ColorName");
}
}
public MainWindowVM()
{
Selector = new EnumSelectorVM
(
typeof(MyColors),
MyColors.Red,
false,
val => ColorName = "The color is " + ((MyColors)val).ToString()
);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The class that does all the work inherits from DynamicObject. Viewed from the outside it creates a bool property for each element in the enum prefixed with 'Is', 'IsRed', 'IsBlue' etc. that can be bound to from XAML. Along with a Value property that holds the actual enum value.
public enum MyColors
{
Red,
Magenta,
Green,
Cyan,
Blue,
Yellow
}
EnumSelectorVM
using System;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
namespace EnumSelectorTest
{
public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged
{
//------------------------------------------------------------------------------------------------------------------------------------------
#region Fields
private readonly Action<object> _action;
private readonly Type _enumType;
private readonly string[] _enumNames;
private readonly bool _notifyAll;
#endregion Fields
//------------------------------------------------------------------------------------------------------------------------------------------
#region Properties
private object _value;
public object Value
{
get { return _value; }
set
{
if (_value == value) return;
_value = value;
RaisePropertyChanged("Value");
_action?.Invoke(_value);
}
}
#endregion Properties
//------------------------------------------------------------------------------------------------------------------------------------------
#region Constructor
public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action<object> action = null)
{
if (!enumType.IsEnum)
throw new ArgumentException("enumType must be of Type: Enum");
_enumType = enumType;
_enumNames = enumType.GetEnumNames();
_notifyAll = notifyAll;
_action = action;
//do last so notification fires and action is executed
Value = initialValue;
}
#endregion Constructor
//------------------------------------------------------------------------------------------------------------------------------------------
#region Methods
//---------------------------------------------------------------------
#region Public Methods
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string elementName;
if (!TryGetEnumElemntName(binder.Name, out elementName))
{
result = null;
return false;
}
try
{
result = Value.Equals(Enum.Parse(_enumType, elementName));
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
{
result = null;
return false;
}
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object newValue)
{
if (!(newValue is bool))
return false;
string elementName;
if (!TryGetEnumElemntName(binder.Name, out elementName))
return false;
try
{
if((bool) newValue)
Value = Enum.Parse(_enumType, elementName);
}
catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
{
return false;
}
if (_notifyAll)
foreach (var name in _enumNames)
RaisePropertyChanged("Is" + name);
else
RaisePropertyChanged("Is" + elementName);
return true;
}
#endregion Public Methods
//---------------------------------------------------------------------
#region Private Methods
private bool TryGetEnumElemntName(string bindingName, out string elementName)
{
elementName = "";
if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0)
return false;
var name = bindingName.Remove(0, 2); // remove first 2 chars "Is"
if (!_enumNames.Contains(name))
return false;
elementName = name;
return true;
}
#endregion Private Methods
#endregion Methods
//------------------------------------------------------------------------------------------------------------------------------------------
#region Events
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion Events
}
}
To respond to changes you can either subscribe to the NotifyPropertyChanged event or pass an anonymous method to the constructor as done above.
And finally the MainWindow.xaml
<Window x:Class="EnumSelectorTest.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"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<RadioButton IsChecked="{Binding Selector.IsRed}">Red</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsMagenta}">Magenta</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsBlue}">Blue</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsCyan}">Cyan</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsGreen}">Green</RadioButton>
<RadioButton IsChecked="{Binding Selector.IsYellow}">Yellow</RadioButton>
<TextBlock Text="{Binding ColorName}"/>
</StackPanel>
</Grid>
</Window>
Hope someone else finds this useful, 'cause I reckon this ones going in my toolbox.
You have to add the Group Name for the Radio button
<StackPanel>
<RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" GroupName="groupName" />
<RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" GroupName="groupName" />
</StackPanel>
I have a very similar problem in VS2015 and .NET 4.5.1
XAML:
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="6" Rows="1"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate >
<RadioButton GroupName="callGroup" Style="{StaticResource itemListViewToggle}" Click="calls_ItemClick" Margin="1" IsChecked="{Binding Path=Selected,Mode=TwoWay}" Unchecked="callGroup_Checked" Checked="callGroup_Checked">
....
As you can see in this code i have a listview, and items in template are radiobuttons that belongs to a groupname.
If I add a new item to the collection with the property Selected set to True it appears checked and the rest of buttons remain checked.
I solve it by getting the checkedbutton first and set it to false manually but this is not the way it's supposed to be done.
code behind:
`....
lstInCallList.ItemsSource = ContactCallList
AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change
.....
Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs)
'Whenever collection change we must test if there is no selection and autoselect first.
If e.Action = NotifyCollectionChangedAction.Add Then
'The solution is this, but this shouldn't be necessary
'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList)
'If seleccionado IsNot Nothing Then
' seleccionado.IsChecked = False
'End If
DirectCast(e.NewItems(0), PhoneCall).Selected = True
.....
End sub
`
<RadioButton IsChecked="{Binding customer.isMaleFemale}">Male</RadioButton>
<RadioButton IsChecked="{Binding customer.isMaleFemale,Converter= {StaticResource GenderConvertor}}">Female</RadioButton>
Below is the code for IValueConverter
public class GenderConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return !(bool)value;
}
}
this worked for me. Even value got binded on both view and viewmodel according to the radio button click. True--> Male and False-->Female

WPF-Custom TabItem with Delete Button Binding Issue

Hi i' m trying to create a custom TabItem with delete Button,i want to bind my viewmodel command to my custom Dependency Property 'DeleteCommandProperty' . Can someone tell me what i'm doing wrong?
my Custom TabControl :
/// <summary>
/// TabControl withCustom TabItem
/// </summary>
public class MyTabControl:TabControl
{
/// <summary>
/// TabItem override
/// </summary>
/// <returns></returns>
protected override DependencyObject GetContainerForItemOverride()
{
return new MyTabItem();
}
}
my Custom TabItem class :
/// <summary>
/// Custom TabItem
/// </summary>
public class MyTabItem:TabItem
{
/// <summary>
/// Delete Command
/// </summary>
public static DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
"DeleteCommand",typeof(ICommand),typeof(MyTabItem));
/// <summary>
/// Delete
/// </summary>
public ICommand DeleteCommand
{
get { return (ICommand)GetValue(DeleteCommandProperty); }
set { SetValue(DeleteCommandProperty, value); }
}
}
when i bind the DeleteCommand directly like this my Command in my ViewModel is executed
<customControls:MyTabControl>
<customControls:MyTabItem Header="Test" DeleteCommand="{Binding DeleteStudiengangCommand}" Template="{DynamicResource MyTabItemControlTemplate}"/>
</customControls:MyTabControl>
bu when try to bind the deleteCommand via style like this but it doesn't work :
<Style TargetType="customControls:MyTabItem">
<Setter Property="Template" Value="{DynamicResource MyTabItemControlTemplate}"/>
<Setter Property="DeleteCommand" Value="{Binding MyDeleteCommand}"/>
</Style>
<customControls:MyTabControl ItemsSource="{Binding MyList}" SelectedItem="{Binding SelectedItem}" SelectedIndex="0">
<customControls:MyTabControl.ContentTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Value}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</customControls:MyTabControl.ContentTemplate>
</customControls:MyTabControl>
TL;DR
I suspect your DataContext contains your current item from MyList (whatever it is), so the style setter cannot access the MyDeleteCommand property as you planned. Look at the visual studio debugger log, whether a binding exception is logged there.
Example that evolved from a working code until it happened to reveal a possible problem explanation.
It seems you have some difficulties in reducing your example, so the only thing I can offer you based on the information you provide is a small working example with the Style and TemplateBinding approach which you can use as a base to identify your real problem. Edit: The explanation at the end might actually be the answer to your question.
Note you may have to change the namespaces to match with your project setup.
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<Window.Resources>
<!-- Header template -->
<ControlTemplate x:Key="MyTabItemControlTemplate" TargetType="{x:Type local:MyTabItem}">
<!-- Some text and the command button with template binding -->
<StackPanel Orientation="Horizontal" Background="Aquamarine" Margin="3">
<ContentPresenter Content="{TemplateBinding Header}" VerticalAlignment="Center" Margin="2"/>
<Button Content="Delete" Command="{TemplateBinding DeleteCommand}" Margin="2"/>
</StackPanel>
</ControlTemplate>
<!-- Setting the control template and assigning the command implementation -->
<Style TargetType="{x:Type local:MyTabItem}">
<Setter Property="Template" Value="{DynamicResource MyTabItemControlTemplate}"/>
<Setter Property="DeleteCommand" Value="{Binding MyDeleteCommand}"/>
<Setter Property="Header" Value="Default Header Text"/>
</Style>
</Window.Resources>
<Grid>
<local:MyTabControl ItemsSource="{Binding MyTabItemList}"/>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// TabControl withCustom TabItem
/// </summary>
public class MyTabControl : TabControl
{
/// <summary>
/// TabItem override
/// </summary>
protected override DependencyObject GetContainerForItemOverride()
{
return new MyTabItem();
}
}
public class MyTabItem : TabItem
{
/// <summary>
/// Delete Command
/// </summary>
public static DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
"DeleteCommand", typeof(ICommand), typeof(MyTabItem));
/// <summary>
/// Delete
/// </summary>
public ICommand DeleteCommand
{
get { return (ICommand)GetValue(DeleteCommandProperty); }
set { SetValue(DeleteCommandProperty, value); }
}
}
public class MyCommand : ICommand
{
public void Execute(object parameter)
{
MessageBox.Show("Hello WPF", "Message");
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged { add { } remove { } }
}
public class MyContext
{
public ICommand MyDeleteCommand { get; set; }
public List<object> MyTabItemList { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var list = new List<object>();
list.Add(new TextBlock() { Text = "Test 1" });
list.Add(new MyTabItem() { Content = "Test Content 2", Header = "Test Header 2" });
list.Add(new TabItem() { Content = "Test Content 3", Header = "Test Header 3" });
this.DataContext = new MyContext()
{
MyTabItemList = list,
MyDeleteCommand = new MyCommand()
};
}
}
Summary of the example:
You see three different tabs, each with its unique build and appearance:
The tab item is created from the GetContainerForItemOverride method, the style setter for the Header property specifies the text that appears within the header. The MyDeleteCommand binding does not work!
The tab item is provided as MyTabItem with header and content value. The style setter for the Header property does not apply, because the property is explicitely set. The style setter for the DeleteCommand property binds to the MyDeleteCommand property from MyContext.
The tab item is provided as TabItem and because there is no override of MyTabControl.IsItemItsOwnContainerOverride, this is accepted as a control item of MyTabControl. The whole MyTabItem template does not apply.
The debugging output inside visual studio gives a hint about the underlying problem for the first tab item:
System.Windows.Data Error: 40 : BindingExpression path error: 'MyDeleteCommand' property not found on 'object' ''TextBlock' (Name='')'. BindingExpression:Path=MyDeleteCommand; DataItem='TextBlock' (Name=''); target element is 'MyTabItem' (Name=''); target property is 'DeleteCommand' (type 'ICommand')
The reason is, that the current tab item content becomes the new local DataContext in this scenario unlike the second tab item, where the item itself is accepted as the container.
A solution could be to ensure usage of the propper context on the command binding:
Supposed you give some suitable parent element a name x:Name="_this", then you can access the parent DataContext.
<Setter Property="DeleteCommand" Value="{Binding DataContext.MyDeleteCommand,ElementName=_this}"/>

Class Deserialisation and WPF bindings

I have a little puzzle I'm trying to solve and am not sure how to go about it...
WPF application based on MVVM approach...
I have a SubstituionDataSet class that inherits from DataSet and defines an additional collection:
namespace Lib
{
public class SubstitutionDataSet : DataSet
{
public SubstitutionDataSet()
{
TableNames = new ObservableCollection<SubstitutionDataTable>();
Tables.CollectionChanging += DataTablesCollectionChanging;
}
public ObservableCollection<SubstitutionDataTable> TableNames { get; set; }
private void DataTablesCollectionChanging(object sender, CollectionChangeEventArgs e)
{
var actionTable = (DataTable) e.Element;
if (e.Action == CollectionChangeAction.Add)
{
actionTable.Columns.CollectionChanged += DataColumnCollectionChanged;
TableNames.Add(new SubstitutionDataTable { Name = actionTable.TableName });
}
else if (e.Action == CollectionChangeAction.Remove)
{
actionTable.Columns.CollectionChanged -= DataColumnCollectionChanged;
TableNames.Remove(TableNames.First(tn => tn.Name == actionTable.TableName));
}
}
private void DataColumnCollectionChanged(object sender, CollectionChangeEventArgs e)
{
var actionColumn = (DataColumn) e.Element;
var hostTable = (DataTable) actionColumn.Table;
var hostSubsitutionTable = TableNames.First(tn => tn.Name == hostTable.TableName);
if (e.Action == CollectionChangeAction.Add)
{
hostSubsitutionTable.ColumnNames.Add(actionColumn.ColumnName);
}
else if (e.Action == CollectionChangeAction.Remove)
{
hostSubsitutionTable.ColumnNames.Remove(hostSubsitutionTable.ColumnNames.First(cn => cn == actionColumn.ColumnName));
}
}
}
}
With the SubstitutionDataTable defined as below:
namespace Lib
{
public sealed class SubstitutionDataTable: INotifyPropertyChanged
{
private string _name;
/// <summary>
/// The <see cref="Name" /> property's name.
/// </summary>
private const string NamePropertyName = "Name";
public SubstitutionDataTable()
{
ColumnNames = new ObservableCollection<string>();
}
/// <summary>
/// Gets the Name property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public string Name
{
get
{
return _name;
}
set
{
if (_name == value)
{
return;
}
_name = value;
RaisePropertyChanged(NamePropertyName);
}
}
public ObservableCollection<string> ColumnNames { get; set; }
#region Implementation of INotifyPropertyChanged
/// <summary>
/// A property has changed - update bindings
/// </summary>
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
...Now this is the crux of the puzzle...
The above classes are used to define a new DataTable within a DataSet and add columns and rows and run-time. I have another Class that allows configuration of an obfuscation process, part of the configuration allows selection of a DataTable and DataColumn from the SubstituionDataSet defined above.
namespace Lib
{
public class ObfuscationParams : INotifyPropertyChanged
{
private string _dataColumn;
private string _dataTable;
private char _maskCharacter;
private int _numberCharacters;
/// <summary>
/// The <see cref="MaskCharacter" /> property's name.
/// </summary>
private const string MaskCharacterPropertyName = "MaskCharacter";
/// <summary>
/// The <see cref="DataColumn" /> property's name.
/// </summary>
private const string DataColumnPropertyName = "DataColumn";
/// <summary>
/// The <see cref="DataTable" /> property's name.
/// </summary>
private const string DataTablePropertyName = "DataTable";
# region Mask Obfuscation Properties
/// <summary>
/// Defines whether whitespace is to be trimmed or not for a Mask obfuscation.
/// </summary>
public bool IsWhiteSpaceTrimmed { get; set; }
/// <summary>
/// Defines the mask character to be used for a Mask obfuscation.
/// </summary>
public char MaskCharacter
{
get { return _maskCharacter; }
set
{
if (_maskCharacter == value)
return;
_maskCharacter = value;
RaisePropertyChanged(MaskCharacterPropertyName);
}
}
/// <summary>
/// Defines the number of masking characters to apply.
/// </summary>
public int NumberCharacters
{
get { return _numberCharacters; }
set { _numberCharacters = value < 1 ? 1 : (value > 16 ? 16 : value); }
}
/// <summary>
/// Defines the mask position for a Mask obfuscation.
/// </summary>
public MaskPosition MaskPosition { get; set; }
#endregion
# region Substitute Obfuscation Properties
/// <summary>
/// Defines which datacolumn is to be used for a Substitution obfuscation.
/// </summary>
public string DataColumn
{
get { return _dataColumn; }
set
{
if (_dataColumn == value)
return;
_dataColumn = value;
RaisePropertyChanged(DataColumnPropertyName);
}
}
/// <summary>
/// Defines which datatable is to be used for a substitition obfuscation.
/// </summary>
public string DataTable
{
get { return _dataTable; }
set
{
if (_dataTable == value)
return;
_dataTable = value;
RaisePropertyChanged(DataTablePropertyName);
_dataTable = value;
}
}
#endregion
#region Implementation of INotifyPropertyChanged
/// <summary>
/// A property has changed - update bindings
/// </summary>
[field: NonSerialized]
public virtual event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
I have the configuration working and can configure a number of obfuscations and then serialize the configuration to disk.
When I deserialize I find the bindings on the GUI don't show the correct DataTable and DataColumn selections, the DataTable just shows the fully qualified object name.
I am currently just trying to get the DataTable binding working - I know I need to rework the DataColumn binding.
The GUI (usercontrol) is defined as below:
<UserControl xmlns:igEditors="http://infragistics.com/Editors" x:Class="SubstitutionOptions"
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="421" d:DesignWidth="395">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="23" />
<RowDefinition Height="23" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Dataset" />
<igEditors:XamComboEditor Grid.Row="0"
Grid.Column="2"
Name="tablesComboBox"
NullText="select a dataset..."
ItemsSource="{Binding DataContext.Project.SubstitutionDataSet.TableNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
DisplayMemberPath="Name"
SelectedItem="{Binding DataContext.SelectedFieldSubstitutionDataTable, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="Column" />
<igEditors:XamComboEditor Grid.Row="1"
Grid.Column="2"
NullText="select a column..."
ItemsSource="{Binding DataContext.SelectedFieldSubstitutionDataTable.ColumnNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataColumn, Mode=TwoWay}"/>
</Grid>
</UserControl>
I hope I have explained the problem sufficiently. Has anyone got any ideas on how I can either get it working using the current design or redesign the approach to achieve what I need?
OK, think I've cracked it now, not that anyone seems interested :-)
I'll post the answer for posterity though...
I changed the bindings for the Comboboxes like so...
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Dataset" />
<igEditors:XamComboEditor Grid.Row="0"
Grid.Column="2"
NullText="select a dataset..."
ItemsSource="{Binding DataContext.VDOProject.SubstitutionDataSet.TableNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
DisplayMemberPath="Name"
Text="{Binding DataTable, Mode=TwoWay}"
SelectedItem="{Binding DataContext.SelectedFieldSubstitutionDataTable, Mode=OneWayToSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="Column" />
<igEditors:XamComboEditor Grid.Row="1"
Grid.Column="2"
NullText="select a column..."
ItemsSource="{Binding DataContext.SelectedFieldSubstitutionDataTable.ColumnNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
Text="{Binding DataColumn, Mode=TwoWay}" />

Categories