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;
}
}
Related
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
Fellow WPF coders, I am not unable to bind the source of an image using a BitMapImage property( ImageSource) in the ViewModel. I implemented INotifyPropertyChanged on this property, I looked up several pages on this website and tried the solutions, The snoop tool shows that the property binding was correct with the path to the image file in resources. But I can't see the image on each list view item when I select it. Please take a look at the BindImages Function and the xaml for the image . I also tried moving the property to the the actual object in the myList collection. But I still don't see the image at run time.
Here is the code:
<Window x:Class="TestProject.View.MyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:me="clr-namespace:TestProject.Model"
xmlns:local="clr-namespace:TestProject.Framework.Converter"
xmlns:view="clr-namespace:TestProject.View"
xmlns:viewmodel="clr-namespace:TestProject.ViewModel"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
DataContext="{Binding Test, Source={StaticResource Locator}}"
Title="Test Overview" Height="300" Width="500" Icon="/TestProject;component/Resources/TestProject.png" Background="{StaticResource {x:Static SystemColors.ControlBrushKey}}" WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:ImageConverter x:Key="imageConverter"/>
</Window.Resources>
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="220" />
</Grid.ColumnDefinitions>
<Menu Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Close" />
</MenuItem>
<MenuItem Header="Edit">
<MenuItem Header="Select" Command ="{Binding SelectCmd}" IsEnabled="{Binding Path=SelectedIndex, ElementName=listView, Converter={StaticResource ResourceKey=enableConverter}}"
ToolTip="Selects the test so the results are displayed in the main window for quick reference." >
<MenuItem.Icon>
<Image Source="/TestProject;component/Resources/Testimage.gif" Height="16" Width="16" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<ListView Name="listView" ItemsSource="{Binding Source={myList}}" SelectedItem="{Binding SelectedTest, Mode=TwoWay}" SelectionMode="Single" Grid.RowSpan="2" Grid.Row="1" >
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Select" Name="menuSelect" Command ="{Binding SelectCmd}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu},
Path=PlacementTarget.DataContext}"
ToolTip="Selects the test so the results are displayed in the main window for quick reference.">
<MenuItem.Icon>
<Image Source="/TestProject;component/Resources/Testimage.gif" Height="16" Width="16" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListView.ContextMenu>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Width="12" Height="12" Source="{Binding ImageSource}" DataContext="{Binding Test, Source={StaticResource Locator}}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ImageConverter.cs
public sealed class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
try
{
return new BitmapImage(new Uri((string)value));
}
catch
{
return new BitmapImage();
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
TestViewmodel.cs
public class TestViewModel
{
#region Fields
private BitmapImage _imagesource;
#endregion
#region Properties
public BitmapImage ImageSource
{
get
{
return _imagesource;
}
set
{
_imagesource = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("ImageSource");
}
}
#endregion
#region Relay Commands
/// <summary>
/// Command that selects the test in the list of the tests.
/// </summary>
public RelayCommand SelectCmd { get; private set; }
/// <summary>
/// Command to select the tested subdivision from the context menu of the list view
/// </summary>
public RelayCommand<MenuClass> ContextSelectCmd { get; private set; }
#endregion
public TestViewModel()
{
_imagesource = new BitmapImage();
TestProject.Instance.PropertyChanged += TestProject_SelectedChange_Changed;
ContextSelectCmd = new RelayCommand<MenuClass>(SelectMenuItem);
SelectCmd = new RelayCommand(() => SelectTest(), () => true);
}
private void TestProject_SelectedChange_Changed(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
try
{
if (e.PropertyName == "SelectedTest")
{
//Select the selected item.
Dispatcher.CurrentDispatcher.DynamicInvoke(BindImages);
}
}
catch (Exception ex)
{
ErrorLogger.Log(LogLevel.Error, ex.ToString());
}
}
/// <summary>
/// Sets the selection checkmarks.
/// </summary>
public void BindImages()
{
List<Test> myTests= new List<Test>();
foreach (var item in TestProject.Instance.TestHistory.Values)
{
myTests = item;
foreach (Test test in myTests)
{
if (TestProject.Instance.SelectedTest.ContainsKey(test.Order.Id))
{
if (TestProject.Instance.SelectedTest[test.Order.Id] == test)
{
Dispatcher.CurrentDispatcher.DynamicInvoke(delegate()
{
BitmapImage logo = new BitmapImage();
logo.BeginInit();
logo.UriSource = new Uri(#"/TestProject;component/Resources/Testimage.gif", UriKind.Relative);
logo.EndInit();
Imagesource = logo;
});
}
}
}
}
}
private void SelectMenuItem ( MenuClass m )
{
try
{
Test t = SelectedTest as Test;
if (t != null)
{
Test tst = t;
if (TestProject.Instance.SelectedTest.ContainsKey(tst.Order.Id))
{
TestProject.Instance.SelectedTest[tst.Order.Id] = tst;
}
else
{
TestProject.Instance.SelectedTest.Add(tst.Order.Id, tst);
}
TestProject.Instance.OnPropertyChanged("SelectedTest");
}
}
catch (Exception ex)
{
ErrorLogger.Log(LogLevel.Error, ex.ToString());
}
}
#region INotifyPropertyChanged Members
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Safely raises the PropertyChanged event.
/// </summary>
/// <param name="property">The property name.</param>
protected void OnPropertyChanged(string Status)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(Status));
}
}
#endregion
Your TestViewModel class doesn't implement INotifyPropertyChanged interfrace.
Add :INotifyPropertyChanged after public class TestViewModel.
Looking at your Converter code where you are expecting a string value, your ImageSource property should be string type. And Even if you have taken ImageSource property as BitmapImage, you are not setting it, you are setting, _imagesource variable which won't send any notification to the view. For notification, you should set, ImageSource property instead of _imagesource variable. Also if you are taking ImageSource property as BitmapImage, you don't need Converter, you can directly bind this property with the Source property of Image.
I got this resolved finally. All I had to do was to create an ImageString string property in my Test object not in the ViewModel. I set this to string .empty, but when the binding happens, I assigned the URI path to it.
Test.cs
/// <summary>
/// Represents a test validation result.
/// </summary>
internal class Test : INotifyPropertyChanged
{
private string _imgstring = string.Empty;
public string ImageString
{
get
{
return _imgstring;
}
set
{
_imgstring = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("ImageString");
}
}
}
TestViewModel.cs
/// <summary>
/// Sets the selection checkmarks.
/// </summary>
public void BindImages()
{
List<Test> myTests= new List<Test>();
foreach (var item in TestProject.Instance.TestHistory.Values)
{
myTests = item;
foreach (Test test in myTests)
{
if (TestProject.Instance.SelectedTest.ContainsKey(test.Order.Id))
{
if (TestProject.Instance.SelectedTest[test.Order.Id] == test)
{
Dispatcher.CurrentDispatcher.DynamicInvoke(delegate()
{
test.ImageString=#"/TestProject;component/Resources/Testimage.gif"
});
}
}
}
}
}
xaml:
<Image Width="12" Height="12" Source="{Binding ImageString}"/>
When i first load the window, the button is visible and there isn't an error in the validation (no red line round textbox).
When typing values into the textbox, the validation rules work as they should.
I would like, if possible, to have the button hidden at the start, and for the validation rules to start when the I start typing text into the box.
Here is the code that i have so far. The xaml:
<TextBox x:Name="txtName" HorizontalAlignment="Left" Height="23" Margin="156,119,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged" NotifyOnSourceUpdated="True">
<Binding.ValidationRules>
<local:ValidationTest/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="btn1" Content="Button" HorizontalAlignment="Left" Margin="85,221,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=txtName}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"></Setter>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
My validation logic:
class ValidationTest : ValidationRule
{
private int result;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null || string.IsNullOrEmpty(value.ToString()))
{
return new ValidationResult(false, "Value cannot be empty.");
}
if (value.ToString().Length > 4)
{
return new ValidationResult(false, "Name cannot be more than 20 characters long.");
}
return ValidationResult.ValidResult;
}
}
The error template i am using:
<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1.5">
<AdornedElementPlaceholder x:Name="ErrorAdorner"></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Window.Resources>
I have tried to update the binding when the window loads by using txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();, but that shows the validation error (red line around textbox). However, the button is hidden, so is there any way of hiding the validation error until the user types text into the textbox?
Am i approaching this in the right way?
I know I am a little late to the party but I came across this question while I was looking to do the same thing. The only thing that I didn't like about using a flag to control when the Validation is performed is that you needed to set the DoValidation flag at some point in your code and I wanted it to be a little more "Automated".
I found quite a few examples online but they all seemed to use the Boolean flag method. I found this MSDN Article and used it as a base then adjusted the code.
I came up with a solution that seems to work really well. Basically in a nutshell what I did was instead of having one variable to keep track of when the validation should be performed I created another Dictionary to keep track of:
When the Validation should be performed.
Store the state of the Validation (Valid, Invalid).
I only wanted the validation to be performed after the first update, so the first order of business is to decide if the Validation should be performed. The first run of the Validation stores the parameter in the Dictionary, then next time around if the parameter is present it performs the validation and stores a true/false (Invalid/Valid) result. This is also a handy way of telling both if the Model has been Validated and if it is valid, so I also added a parameter/flag to simply return if there are any results and the state of the validation. This is especially useful for binding the command enable/disable.
Here is how I accomplished this:
Base PropertyValidation Model:
public abstract class PropertyValidation : INotifyPropertyChanged, IDataErrorInfo
{
#region Fields
private readonly Dictionary<string, object> _values = new Dictionary<string, object>();
/// <summary>
/// This holds the list of validation results and controls when the validation should be
/// performed and if the validation is valid.
/// </summary>
private Dictionary<string, bool> _validationResults { get; set; } = new Dictionary<string, bool>();
#endregion
#region Protected
/// <summary>
/// Sets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertySelector">Expression tree contains the property definition.</param>
/// <param name="value">The property value.</param>
protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
{
string propertyName = GetPropertyName(propertySelector);
SetValue<T>(propertyName, value);
}
/// <summary>
/// Sets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <param name="value">The property value.</param>
protected void SetValue<T>(string propertyName, T value)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
_values[propertyName] = value;
OnPropertyChanged(propertyName);
}
/// <summary>
/// Gets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertySelector">Expression tree contains the property definition.</param>
/// <returns>The value of the property or default value if not exist.</returns>
protected T GetValue<T>(Expression<Func<T>> propertySelector)
{
string propertyName = GetPropertyName(propertySelector);
return GetValue<T>(propertyName);
}
/// <summary>
/// Gets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <returns>The value of the property or default value if not exist.</returns>
protected T GetValue<T>(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
object value;
if (!_values.TryGetValue(propertyName, out value))
{
value = default(T);
_values.Add(propertyName, value);
}
return (T)value;
}
/// <summary>
/// Validates current instance properties using Data Annotations.
/// </summary>
/// <param name="propertyName">This instance property to validate.</param>
/// <returns>Relevant error string on validation failure or <see cref="System.String.Empty"/> on validation success.</returns>
protected virtual string OnValidate(string propertyName)
{
string error = string.Empty;
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
//Check if the Field has been added, this keeps track of when the validation
//is performed.
if (_validationResults.Any(x => x.Key == propertyName))
{
var value = GetValue(propertyName);
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
var result = Validator.TryValidateProperty(
value,
new ValidationContext(this, null, null)
{
MemberName = propertyName
},
results);
if (!result)
{
var validationResult = results.First();
error = validationResult.ErrorMessage;
//Store a true result in the validation to set the error.
_validationResults[propertyName] = true;
}
else
{
//If the Validation has been run and not invalid make sure the
//paramter in the list is cleared, otherwise validation would
//always return invalid once it is invalidated.
_validationResults[propertyName] = false;
}
}
else
{
//This is the first run of the Validation, simply store the paramter
//in the validation list and wait until next time to validate.
_validationResults.Add(propertyName, true);
}
//Notify that things have changed
OnPropertyChanged("IsValid");
//Return the actual result
return error;
}
#endregion
#region Public
/// <summary>
/// This returns if the Validation is Valid or not
/// </summary>
/// <returns>True if the Validation has been perfomed and if there are not
/// true values. Will return false until the validation has been done once.</returns>
public bool IsValid {
get { return (!_validationResults.Any(x => x.Value) && (_validationResults.Count > 0)); }
}
/// <summary>
/// Clears/Reset the Validation
/// </summary>
public void ClearValidation()
{
_validationResults.Clear();
}
#endregion
#region Change Notification
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
protected void OnPropertyChanged<T>(Expression<Func<T>> propertySelector)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
string propertyName = GetPropertyName(propertySelector);
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion // IOnPropertyChanged Members
#region Data Validation
string IDataErrorInfo.Error {
get {
throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
}
}
string IDataErrorInfo.this[string propertyName] {
get {
return OnValidate(propertyName);
}
}
#endregion
#region Privates
private string GetPropertyName(LambdaExpression expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
throw new InvalidOperationException();
}
return memberExpression.Member.Name;
}
private object GetValue(string propertyName)
{
object value;
if (!_values.TryGetValue(propertyName, out value))
{
var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
if (propertyDescriptor == null)
{
throw new ArgumentException("Invalid property name", propertyName);
}
value = propertyDescriptor.GetValue(this);
_values.Add(propertyName, value);
}
return value;
}
#endregion
#region Debugging
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
}
Model:
public class MyModel : PropertyValidation
{
[Required(ErrorMessage = "Name must be specified")]
[MaxLength(50, ErrorMessage = "Name too long, Name cannot contain more than 50 characters")]
public string Name {
get { return GetValue(() => Name); }
set { SetValue(() => Name, value); }
}
[Required(ErrorMessage = "Description must be specified")]
[MaxLength(150, ErrorMessage = "Description too long, Description cannot contain more than 150 characters")]
public string Description {
get { return GetValue(() => Description); }
set { SetValue(() => Description, value); }
}
}
Error Template:
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel LastChildFill="true">
<Border Background="Red" DockPanel.Dock="right"
Margin="-20,0,0,0" Width="10" Height="10" CornerRadius="10"
ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
<TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white"/>
</Border>
<AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
<Border BorderBrush="red" BorderThickness="1" >
<Border.Effect>
<BlurEffect Radius="5" />
</Border.Effect>
</Border>
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
Data Template/Form:
<DataTemplate x:Key="MyModelDetailsTemplate" DataType="{x:Type data:MyModel}" >
<StackPanel Grid.IsSharedSizeScope="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">Name</Label>
<TextBox x:Name="Name"
Grid.Column="1"
MinWidth="150"
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">Description</Label>
<TextBox Grid.Column="1"
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" AcceptsReturn="True" VerticalAlignment="Stretch" />
</Grid>
</StackPanel>
</DataTemplate>
RelayCommand (For Completeness)
public class RelayCommand : ICommand
{
private Action<object> execute;
private Predicate<object> canExecute;
private event EventHandler CanExecuteChangedInternal;
public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
if (canExecute == null)
{
throw new ArgumentNullException("canExecute");
}
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged {
add {
CommandManager.RequerySuggested += value;
this.CanExecuteChangedInternal += value;
}
remove {
CommandManager.RequerySuggested -= value;
this.CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter)
{
return this.canExecute != null && this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler handler = this.CanExecuteChangedInternal;
if (handler != null)
{
//DispatcherHelper.BeginInvokeOnUIThread(() => handler.Invoke(this, EventArgs.Empty));
handler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
this.canExecute = _ => false;
this.execute = _ => { return; };
}
private static bool DefaultCanExecute(object parameter)
{
return true;
}
}
ViewModel:
** Note the PropertyValidation here is not required, A separate INotifyPropertyChanged base could be used instead, I only used it for the OnPropertyChanged Notification and to keep things simple **
public class PageHomeVM : PropertyValidation
{
private ICommand saveCommand;
public ICommand SaveCommand {
get {
return saveCommand;
}
set {
saveCommand = value;
OnPropertyChanged();
}
}
public MyModel MyModel { get; set; } = new MyModel();
public PageHomeVM()
{
SaveCommand = new RelayCommand(SaveRecord, p => MyModel.IsValid);
MyModel.ClearValidation();
}
public void SaveRecord(object p)
{
//Perform the save....
}
}
View:
<pages:BasePage.DataContext>
<ViewModels:PageHomeVM/>
</pages:BasePage.DataContext>
<StackPanel>
<Label Content="MyModel Details"/>
<ContentPresenter ContentTemplate="{StaticResource MyModelDetailsTemplate}" Content="{Binding MyModel}" />
<Button x:Name="btnSave"
Command="{Binding SaveCommand}"
Width="75"
HorizontalAlignment="Right">Save</Button>
</StackPanel>
I hope this helps...
save a flag for once you have any value
class ValidationTest : ValidationRule
{
private int result;
private bool hadValue = false;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (hadValue && (value == null || string.IsNullOrEmpty(value.ToString())))
{
return new ValidationResult(false, "Value cannot be empty.");
}
if (value.ToString().Length > 4)
{
hadValue = true;
return new ValidationResult(false, "Name cannot be more than 20 characters long.");
}
hadValue = true;
return ValidationResult.ValidResult;
}
}
I have a UserControl that includes three TextBlock controls. I want to implement three custom properties in UserControl. Something like:
public partial class MyControl: UserControl
{
...
public String Title
{
get { return this.textBlock1.Text; }
set { this.textBlock1.Text = value; }
}
public String Units
{
get { return this.textBlock2.Text; }
set { this.textBlock2.Text = value; }
}
public String Data
{
get { return this.textBlock3.Text; }
set { this.textBlock3.Text = value; }
}
}
If I want to use binding capabilities with these properties I have to implement them as dependency properties. Am I right? But I do not know how to do it in my case.
That is correct. Binding to dependency properties is quite simple to do. Understanding the mechanics I would suggest looking through MSDN. However to answer your question you provide static dependency properties registered to the user control. Then your getters \ setters reference the property.
Here is a sample line of a dependency property.
/// <summary>
/// Provides a bindable text property to the user control
/// </summary>
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(UserControl1), new PropertyMetadata("", onTextPropertyChanged));
/// <summary>
/// optional static call back handler when the property changed
/// </summary>
/// <param name="o"></param>
/// <param name="e"></param>
static void onTextPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var obj = o as UserControl1;
if (obj == null)
return;
//TODO: Changed...
}
/// <summary>
/// Gets \ sets the text
/// </summary>
public string Text
{
get { return (string)this.GetValue(TextProperty); }
set
{
if (this.Text != value)
this.SetValue(TextProperty, value);
}
}
The above is very simple. We register a dependency property TextProperty to UserControl1, this property is the type of string and has a default value of "" (as noted in the property meta data). I also provided a static callback handler if you wish to perform additional steps once the property has changed.
You will then see the Text property uses the GetValue() and SetValue() methods for getting and setting the value of the Text Property.
UPDATE: Binding to a child element in XAML.
This update is to show how to use the the above TextProperty for binding.
Usercontrol1.Xaml. This is the XAML for UserControl1.
<UserControl x:Class="WpfApplication1.UserControl1"
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"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" FontWeight="Bold" Content="Text" VerticalAlignment="Center" />
<TextBox Text="{Binding Text, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" Padding="4" />
</Grid>
</UserControl>
My Main Window View Model (Implement INotifyPropertyChanged)
public class MainWindowModel : INotifyPropertyChanged
{
/// <summary>
/// the text
/// </summary>
string myProperty = "This is the default text";
/// <summary>
/// Gets \ sets the text
/// </summary>
public string MyProperty
{
get { return this.myProperty; }
set
{
if (this.MyProperty != value)
{
this.myProperty = value;
this.OnPropertyChanged("MyProperty");
}
}
}
/// <summary>
/// fires the property changed event
/// </summary>
/// <param name="propertyName"></param>
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// the property changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
}
MainWindow.Xaml. Binding the text property to the view model
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:WpfApplication1"
Name="Window1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ctrl:MainWindowModel />
</Window.DataContext>
<Grid>
<ctrl:UserControl1 Text="{Binding Path=DataContext.MyProperty, Mode=TwoWay, ElementName=Window1}" />
</Grid>
</Window>
Code for the dependency property :
public string Title
{
get { return (string)this.GetValue(TitleProperty); }
set { this.SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title",
typeof(string),
typeof(MyControl),
new PropertyMetadata(null));
Binding in xaml :
<TextBlock Text="{Binding Title,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type yourXmlns:MyControl}}"/>
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}" />