I've created my own UserControl, called PersonNameControl which is intented to be reused.
The control has three TextBox fields, and has three dependency properties in its class file.
Firstname
Insertion
Lastname
Each dependency property value is bound to to a field, so the dependency property Firstname is bound to the Firstname TextBox, and so on.
I conciously didn't explicitly set the DataContext of the UserControl.
The control should be as loosely as possible. It should only get it's values (for the fields) via its dependency properties. It shouldn't even be looking to anything like DataContext.
<UserControl x:Class="WpfApplication1.PersonNameControl">
<StackPanel>
<Label>Firstname:</Label>
<TextBox Text="{Binding Firstname, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
<Label>Insertion:</Label>
<TextBox Text="{Binding Insertion, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
<Label>Lastname:</Label>
<TextBox Text="{Binding Lastname, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
</TextBox>
</StackPanel>
</UserControl>
And the control class:
public partial class PersonNameControl : UserControl
{
public PersonNameControl()
{
InitializeComponent();
}
public string Firstname
{
get { return (string)GetValue(FirstnameProperty); }
set { SetValue(FirstnameProperty, value); }
}
public static readonly DependencyProperty FirstnameProperty =
DependencyProperty.Register("Firstname", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string Insertion
{
get { return (string)GetValue(InsertionProperty); }
set { SetValue(InsertionProperty, value); }
}
public static readonly DependencyProperty InsertionProperty =
DependencyProperty.Register("Insertion", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string Lastname
{
get { return (string)GetValue(LastnameProperty); }
set { SetValue(LastnameProperty, value); }
}
public static readonly DependencyProperty LastnameProperty =
DependencyProperty.Register("Lastname", typeof(string), typeof(PersonNameControl),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
The control is supposed to be used inside another view as follows:
<!--
Here we are inside a view or some other control.
The bindings here provide the dependency properties of the UserControl with a value.
The DataContext of the view where my UserControl is used, is a ViewModel that implements INotifyDataErrorInfo
-->
<myControls:PersonNameControl
Firstname="{Binding SomeFirstnameFromVM, Mode=TwoWay}"
Insertion="{Binding SomeInsertionFromVM, Mode=TwoWay}"
Lastname="{Binding SomeLastnameFromVM, Mode=TwoWay}">
</myControls:PersonNameControl>
When the ViewModel (implements INotifyDataErrorInfo) created a Validation error, nothing happens with my PersonNameControl UserControl.
I managed in making a control that is independent because it doesn't rely on a specific DataContext, doesn't set its own DataContext in its codebehind file, and just gets its values via dependency properties. The values are exchanged via the bindings and show up, but the validation errors don't show.
What I want is passing the validation errors through, to the UserControl.
Some solutions on the internet make use of ValidationAdornerSite and I tried this. But this would only work for one TextBox.
I don't see any solution without making my control dependent on the outside world or introducing ugly extra properties to solve it cumbersome. I thought the errors are 'tunneled' like a piece of information through all bindings towards the last level where the value arrives. But this seems not to be the right consideration.
Edit:
I added my ViewModel class.
public class CustomerFormViewModel : ViewModelBase, INotifyDataErrorInfo
{
protected string _clientNumber;
protected DateTime _date;
protected string _firstname;
protected string _insertion;
protected string _lastname;
protected Address _address;
protected ObservableCollection<Email> _emails;
protected ObservableCollection<PhoneNumber> _phoneNumbers;
protected string _note;
protected bool _hasErrors;
protected IList<ValidationFailure> _validationErrors;
public IList<ValidationFailure> ValidationErrors
{
get { return _validationErrors; }
set { _validationErrors = value; OnPropertyChanged("ValidationErrors"); }
}
public string ClientNumber
{
get { return _clientNumber; }
set { _clientNumber = value; OnPropertyChanged("ClientNumber"); }
}
public DateTime Date
{
get { return _date; }
set { _date = value; OnPropertyChanged("Date"); }
}
public string Firstname
{
get { return _firstname; }
set { _firstname = value; OnPropertyChanged("Firstname"); }
}
public string Insertion
{
get { return _insertion; }
set { _insertion = value; OnPropertyChanged("Insertion"); }
}
public string Lastname
{
get { return _lastname; }
set { _lastname = value; OnPropertyChanged("Lastname"); }
}
public Address Address
{
get { return _address; }
set { _address = value; OnPropertyChanged("Address"); }
}
public ObservableCollection<Email> Emails
{
get { return _emails; }
set { _emails = value; OnPropertyChanged("Emails"); }
}
public ObservableCollection<PhoneNumber> PhoneNumbers
{
get { return _phoneNumbers; }
set { _phoneNumbers = value; OnPropertyChanged("PhoneNumbers"); }
}
public string Note
{
get { return _note; }
set { _note = value; OnPropertyChanged("Note"); }
}
private DelegateCommand _saveCustomerCommand;
public DelegateCommand SaveCustomerCommand
{
get { return _saveCustomerCommand; }
private set { _saveCustomerCommand = value; OnPropertyChanged("SaveCustomerCommand"); }
}
public CustomerFormViewModel()
{
ValidationErrors = new List<ValidationFailure>();
SaveCustomerCommand = new DelegateCommand(SaveCustomer, CanSaveCustomer);
}
protected void ValidateInput()
{
ValidationErrors.Clear();
CustomerFormValidator validator = new CustomerFormValidator();
FluentValidation.Results.ValidationResult result = validator.Validate(this);
ValidationErrors = result.Errors;
foreach (ValidationFailure f in ValidationErrors)
{
Console.WriteLine(f.ErrorMessage);
}
_hasErrors = result.Errors.Count != 0;
List<string> vmProperties = new List<string>() { "Firstname", "Lastname", "Address", "ClientNumber", "Date" };
foreach (string propertyName in vmProperties)
{
OnErrorsChanged(propertyName);
}
}
public bool HasErrors
{
get { return _hasErrors; }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected void OnErrorsChanged(string name)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(name));
}
public IEnumerable GetErrors(string propertyName)
{
return ValidationErrors.Where<ValidationFailure>(x => x.PropertyName == propertyName);
}
public void SaveCustomer(object parameter)
{
this.ValidateInput();
if( ! HasErrors)
{
Customer customer = new Customer(-1, ClientNumber, Date, Firstname, Insertion, Lastname, Address);
ICustomerRepository repo = new CustomerRepository();
bool res = repo.SaveCustomer(customer);
if(res) {
// ...
}
// ...
} else
{
MessageBox.Show("One or more fields are not filled in correctly.", "Invalid input", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public bool CanSaveCustomer(object parameter)
{
return true;
}
}
So, I have prepared a demo user control. It is a sub user control, gets all validation info from its MainViewModel
MainWindow
<Window
x:Class="ValidationSubUI.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:local="clr-namespace:ValidationSubUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Name="MyWindow"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<local:SubUserControl
FirstName="{Binding FirstName, Mode=TwoWay}"
LastName="{Binding LastName, Mode=TwoWay}"
ValidationSource="{Binding ElementName=MyWindow, Path=DataContext}" />
</Grid>
</Window>
MainViewModel
using GalaSoft.MvvmLight;
using System.ComponentModel;
namespace ValidationSubUI
{
public class MainViewModel : ViewModelBase, IDataErrorInfo
{
public string Error
{
get
{
return string.Empty;
}
}
private string m_FirstName;
public string FirstName
{
get { return m_FirstName; }
set
{
m_FirstName = value;
RaisePropertyChanged();
}
}
private string m_LastName;
public string LastName
{
get { return m_LastName; }
set
{
m_LastName = value;
RaisePropertyChanged();
}
}
public string this[string columnName]
{
get
{
if (columnName == nameof(FirstName))
{
return GetFirstNameError();
}
else if (columnName == nameof(LastName))
{
return GetLastNameError();
}
return null;
}
}
private string GetFirstNameError()
{
string result = string.Empty;
if (string.IsNullOrEmpty(FirstName))
{
result = "First name required";
}
return result;
}
private string GetLastNameError()
{
string result = string.Empty;
if (string.IsNullOrEmpty(LastName))
{
result = "Last name required";
}
return result;
}
}
}
SubUserControl gets all validation logic from MainViewModel
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace ValidationSubUI
{
/// <summary>
/// Interaction logic for SubUserControl.xaml
/// </summary>
public partial class SubUserControl : UserControl, IDataErrorInfo
{
public SubUserControl()
{
InitializeComponent();
}
public IDataErrorInfo ValidationSource
{
get { return (IDataErrorInfo)GetValue(ValidationSourceProperty); }
set { SetValue(ValidationSourceProperty, value); }
}
// Using a DependencyProperty as the backing store for ValidationSource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValidationSourceProperty =
DependencyProperty.Register("ValidationSource", typeof(IDataErrorInfo), typeof(SubUserControl), new PropertyMetadata(null));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
// Using a DependencyProperty as the backing store for FirstName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));
public string LastName
{
get { return (string)GetValue(LastNameProperty); }
set { SetValue(LastNameProperty, value); }
}
// Using a DependencyProperty as the backing store for LastName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));
public string Error
{
get
{
return string.Empty;
}
}
public string this[string columnName]
{
get
{
if (ValidationSource != null)
{
return ValidationSource[columnName];
}
return null;
}
}
}
}
and SubUserControl
<UserControl
x:Class="ValidationSubUI.SubUserControl"
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"
x:Name="CustomControl"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder x:Name="controlWithError" />
</Border>
<TextBlock
Margin="5,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
FontWeight="DemiBold"
Foreground="Red"
Text="{Binding ElementName=controlWithError, Path=AdornedElement.ToolTip, Mode=OneWay}" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid DataContext="{x:Reference Name=CustomControl}">
<StackPanel>
<TextBox
Width="120"
Height="30"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}"
TextWrapping="Wrap" />
<TextBox
Width="120"
Height="30"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</UserControl>
Related
I have a ViewModel with all the properties that i will need in every sub ViewModel.
It's the first time i try to split commands and viewmodel to multiple files. Last time everything was in the same ViewModel and it was a pain to work with it. Everything shows up as expected but i want to find a way to pass the same data in every viewmodel.
From my GetOrdersCommand, i want to get the HeaderViewModel.SelectedSource property. I didn't find any way to do it without getting a null return or loosing the property data...
I would like to call my GetOrdersCommand from HeaderView button too.
Any tips how i can achieve this ? Perhaps, my design is not good for what i'm trying to do ?
MainWindow.xaml
<views:HeaderView Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" DataContext="{Binding HeaderViewModel}" LoadHeaderViewCommand="{Binding LoadHeaderViewCommand}"/>
<TabControl TabStripPlacement="Bottom" Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="2">
<TabItem Header="General">
</TabItem>
<TabItem Header="Orders">
<views:OrderView DataContext="{Binding OrderViewModel}" GetOrdersCommand="{Binding GetOrdersCommand}"/>
</TabItem>
</TabControl>
HeaderView.xaml
<DockPanel>
<ComboBox DockPanel.Dock="Left" Width="120" Margin="4" VerticalContentAlignment="Center" ItemsSource="{Binding SourceList}" SelectedItem="{Binding SelectedSource}" DisplayMemberPath="SourceName"/>
<Button x:Name="btnTest" HorizontalAlignment="Left" DockPanel.Dock="Left" Margin="4" Content="Test"/>
</DockPanel>
HeaderView.xaml.cs
public partial class OrderView : UserControl
{
public ICommand GetOrdersCommand
{
get { return (ICommand)GetValue(GetOrdersCommandProperty); }
set { SetValue(GetOrdersCommandProperty, value); }
}
public static readonly DependencyProperty GetOrdersCommandProperty =
DependencyProperty.Register("GetOrdersCommand", typeof(ICommand), typeof(OrderView), new PropertyMetadata(null));
public OrderView()
{
InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
if (GetOrdersCommand != null)
{
GetOrdersCommand.Execute(this);
}
}
}
MainViewModel.cs
private OrderViewModel orderViewModel;
public OrderViewModel OrderViewModel { get; set; } // Getter, setter with OnPropertyChanged
private HeaderViewModel headerViewModel;
public HeaderViewModel HeaderViewModel { get; set; } // Getter, setter with OnPropertyChanged
public MainViewModel()
{
HeaderViewModel = new HeaderViewModel();
OrderViewModel = new OrderViewModel();
}
HeaderViewModel.cs
public ICommand LoadHeaderViewCommand { get; set; }
public HeaderViewModel()
{
LoadHeaderViewCommand = new LoadHeaderViewCommand(this);
}
GetOrdersCommand.cs
public class GetOrdersCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private readonly OrderViewModel _orderViewModel;
public GetOrdersCommand(OrderViewModel orderViewModel)
{
_orderViewModel = orderViewModel;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
/* Build Order List according to HeaderViewModel.SelectedSource */
_orderViewModel.Orders = new ObservableCollection<Order>()
{
new Order { ID = 1, IsReleased = false, Name = "Test1"},
new Order { ID = 2, IsReleased = true, Name = "Test2"},
};
}
}
Thanks guys ! I moved my commands to their owning ViewModel as suggested.
I tried MVVVM Light Tools and found about Messenger Class.
I used it to send my SelectedSource (Combobox from HeaderView) from HeaderViewModel to OrderViewModel. Am i suppose to use Messenger class like that ? I don't know, but it did the trick!!!
I thought about moving GetOrdersCommand to OrderViewModel, binding my button command to OrderViewModel, binding SelectedSource as CommandParameter but i didn't know how i was suppose to RaiseCanExecuteChanged when HeaderViewModel.SelectedSource changed... Any advice?
MainWindow.xaml
<views:HeaderView DataContext="{Binding Source={StaticResource Locator}, Path=HeaderVM}" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"/>
<TabControl TabStripPlacement="Bottom" Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="2">
<TabItem Header="General">
</TabItem>
<TabItem Header="Orders">
<views:OrderView DataContext="{Binding Source={StaticResource Locator}, Path=OrderVM}"/>
</TabItem>
</TabControl>
OrderViewModel.cs
private ObservableCollection<Order> _orders;
public ObservableCollection<Order> Orders
{
get { return _orders; }
set
{
if (_orders != value)
{
_orders = value;
RaisePropertyChanged(nameof(Orders));
}
}
}
public OrderViewModel()
{
Messenger.Default.Register<Source>(this, source => GetOrders(source));
}
private void GetOrders(Source source)
{
if (source.SourceName == "Production")
{
Orders = new ObservableCollection<Order>(){
new Order { ID = 1, IsReleased = false, Name = "Production 1" }
};
}
else
{
Orders = new ObservableCollection<Order>(){
new Order { ID = 2, IsReleased = true, Name = "Test 1" }
};
}
}
Part of HeaderViewModel.cs
private Source _SelectedSource;
public Source SelectedSource
{
get { return _SelectedSource; }
set
{
if (_SelectedSource != value)
{
_SelectedSource = value;
RaisePropertyChanged(nameof(SelectedSource));
GetOrdersCommand.RaiseCanExecuteChanged();
}
}
}
private RelayCommand _GetOrdersCommand;
public RelayCommand GetOrdersCommand
{
get
{
if (_GetOrdersCommand == null)
{
_GetOrdersCommand = new RelayCommand(GetOrders_Execute, GetOrders_CanExecute);
}
return _GetOrdersCommand;
}
}
private void GetOrders_Execute()
{
Messenger.Default.Send(SelectedSource);
}
private bool GetOrders_CanExecute()
{
return SelectedSource != null ? true : false;
}
Here is a class with undefined variable that needs to be passed into the WPF window.
public class SelectedVal<T>
{
public T val {get;set;}
}
Window:
public partial class SOMEDialogue : Window
{
public List<SelectedVal<T>> returnlist { get { return FullList; } }
public List<SelectedVal<T>> FullList = new List<SelectedVal<T>>();
public SOMEDialogue (List<SelectedVal<T>> inputVal)
{
InitializeComponent();
}
}
So here is the question, how can I do this properly to get the T and have a global variable set in my WPF?
Edited (code edited too):
The purpose for the WPF is:
A list of SelectedVal<T> input
Display this input in this WPF
Depend on the T type, user can do something about this input
When finished a return List<SelectedVal<T>> returnlist can be
accessed
This is the basic idea I'm describing. Let me know if you hit any snags. I'm guessing that the search text and the min/max int values are properties of the dialog as a whole. I'm also assuming that there may be a mixture of item types in the collection, which may be an assumption too far. Can you clarify that?
Selected value classes
public interface ISelectedVal
{
Object Val { get; set; }
}
public class SelectedVal<T> : ISelectedVal
{
public T Val { get; set; }
object ISelectedVal.Val
{
get => this.Val;
set => this.Val = (T)value;
}
}
public class StringVal : SelectedVal<String>
{
}
public class IntVal : SelectedVal<int>
{
}
Dialog Viewmodel
public class SomeDialogViewModel : ViewModelBase
{
public SomeDialogViewModel(List<ISelectedVal> values)
{
FullList = values;
}
public List<ISelectedVal> FullList { get; set; }
private String _searchText = default(String);
public String SearchText
{
get { return _searchText; }
set
{
if (value != _searchText)
{
_searchText = value;
OnPropertyChanged();
}
}
}
private int _minInt = default(int);
public int MinInt
{
get { return _minInt; }
set
{
if (value != _minInt)
{
_minInt = value;
OnPropertyChanged();
}
}
}
private int _maxInt = default(int);
public int MaxInt
{
get { return _maxInt; }
set
{
if (value != _maxInt)
{
_maxInt = value;
OnPropertyChanged();
}
}
}
}
.xaml.cs
public SOMEDialogue (List<ISelectedVal> inputValues)
{
InitializeComponent();
DataContext = new SomeDialogViewModel(inputValues);
}
XAML
<Window.Resources>
<DataTemplate DataType="{x:Type local:StringVal}">
<StackPanel>
<Label>Value</Label>
<Label Content="{Binding Val}" />
<Label>Search text:</Label>
<TextBox Text="{Binding DataContext.SearchText, RelativeSource={RelativeSource AncestorType=Window}}" />
<!-- Other stuff -->
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:IntVal}">
<StackPanel>
<Label>Value</Label>
<Label Content="{Binding Val}" />
<Label>Min value:</Label>
<TextBox Text="{Binding DataContext.MinIntVal, RelativeSource={RelativeSource AncestorType=Window}}" />
<Label>Max value:</Label>
<TextBox Text="{Binding DataContext.MaxIntVal, RelativeSource={RelativeSource AncestorType=Window}}" />
<!-- Other stuff -->
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl
ItemsSource="{Binding FullList}"
/>
</Grid>
I have model named EditableSong which is derived from ValidatableModel Class which
implements INotifyPropertyChanged and INotifyDataErrorInfo.
class EditableSong : ValidatableModel
{
CommandRelay addcommand = null;
public ICommand AddCommand
{
get { return addcommand; }
}
public EditableSong()
{
addcommand = new CommandRelay(Add, CanAdd);
}
private void Add(object obj = null)
{
MessageBox.Show("Succesfully Added!");
}
private bool CanAdd(object obj = null)
{
return !HasErrors;
}
private string title;
[Required]
[MaxLength(45)]
public string Title
{
get { return title; }
set
{ SetProperty(ref title, value); }
}
private string lyrics;
[MaxLength(3000)]
public string Lyrics
{
get { return lyrics; }
set { SetProperty(ref lyrics, value); }
}
private string artist;
[Required]
public string Artist
{
get { return artist; }
set {SetProperty(ref artist, value); }
}
}
And here is ValidatableModel class:
class ValidatableModel : BindableBase, INotifyDataErrorInfo
{
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public bool HasErrors
{
get { return _errors.Count >0; ; }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected override void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null)
{
base.SetProperty(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
public IEnumerable GetErrors(string propertyName)
{
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
protected void ValidateProperty<T>(string propertyName, T value)
{
var results = new List<ValidationResult>();
ValidationContext context = new ValidationContext(this);
context.MemberName = propertyName;
Validator.TryValidateProperty(value, context, results);
if (results.Any())
{
_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
}
else
{
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
protected void OnErrorsChanged(string propName)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propName));
}
}`
And it works well, but only after I change properties in textboxes of my window.
The main problem is that user mustn't be able to save model without filling required fields, but when windows loads button Save (which uses command) available, because of validation doesn't run.
Here is xaml:
<Window x:Class="ValidationTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ValidationTests"
xmlns:rules="clr-namespace:ValidationTests.Rules"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
<Window.Resources>
<Style x:Key="TextBoxError" TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate x:Name="TextErrorTemplate">
<DockPanel LastChildFill="True">
<AdornedElementPlaceholder>
<Border BorderBrush="Red" BorderThickness="2"/>
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<StackPanel>
<Label>Artist</Label>
<TextBox Style="{StaticResource TextBoxError}" Text="{Binding Artist,
ValidatesOnNotifyDataErrors=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
<Label>Title</Label>
<TextBox Style="{StaticResource TextBoxError}" Text="{Binding Title,
ValidatesOnNotifyDataErrors=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Label>Lyrics</Label>
<TextBox Style="{StaticResource TextBoxError}" Text="{Binding Lyrics,
ValidatesOnDataErrors=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Button Content="Add" Command="{Binding AddCommand}"></Button>
</StackPanel>
</Grid>
I wonder how can i fix this...
I wonder how can i fix this...
You need to perform the actual validation upfront.
Call the ValidateProperty method for all properties in the constructor of your EditableSong class to populate the Dictionary and raise the ErrorsChanged event:
public EditableSong()
{
addcommand = new CommandRelay(Add, CanAdd);
ValidateProperty(nameof(Title), title);
ValidateProperty(nameof(Lyrics), lyrics);
ValidateProperty(nameof(Artist), artist);
}
I'm trying to build a TreeView using MVVM in a WPF App but I don't understand how to handle HierarchicalDataTemplate. My TreeView should represent a folder structure which contains folders within folders and so on.
My folder ViewModel is defined as follows:
public class TreeViewFolderViewModel : ViewModelBase
{
private int _id;
private int _parentId;
private string _text;
private string _key;
private ObservableCollection<TreeViewFolderViewModel> _children;
public int Id
{
get { return this._id; }
set { Set(() => Id, ref this._id, value); }
}
public int ParentId
{
get { return this._parentId; }
set { Set(() => ParentId, ref this._parentId, value); }
}
public string Text
{
get { return this._text; }
set { Set(() => Text, ref this._text, value); }
}
public string Key
{
get { return this._key; }
set { Set(() => Key, ref this._key, value); }
}
public ObservableCollection<TreeViewFolderViewModel> Children
{
get { return this._children ?? (this._children =
new ObservableCollection<TreeViewFolderViewModel>()); }
set { Set(() => Children, ref this._children, value); }
}
}
My model has the same structure as my ViewModel so the final ViewModel is a list of folders that contain child folders and so forth. I'm using recursion to load all these folders and that part is working fine.
Where I'm stuck is on how to define and load this ViewModel into the actual TreeView.
I've read Hierarchical DataBinding in TreeView using MVVM pattern and while I more or less understand what's going on, but each of the levels of the TreeView represent a different object type while my TreeView has only one object type and I'm confused as to how I'm suppose to define this.
The Root ViewModel property in my MainWindowViewModel is of type TreeViewFolderViewModel which means I have a single object which represents to root of my TreeView. This object has Children of type TreeViewFolderViewModel which in turn have also Children of type TreeViewFolderViewModel and so forth
How do I defined this in XAML? I have the following defined:
<TreeView Grid.Row="1" Margin="5,0,5,5" ItemsSource="{Binding RootFolder}"/>
And I've got a Hierarchical template defined as follows:
<Window.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"
DataType="{x:Type viewmodels:SharePointFolderTreeViewViewModel}">
<Label Content="{Binding Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
But nothing is loading up.
Any ideas on how I can resolve this?
Thanks.
I prepared a small sample to illustrate.
ViewModels
public class TreeViewFolderViewModel : ViewModelBase
{
private int id;
public int Id
{
get { return id; }
set { id = value; OnPropertyChanged("Id"); }
}
private string text;
public string Text
{
get { return text; }
set { text = value; OnPropertyChanged("Text"); }
}
private ObservableCollection<TreeViewFolderViewModel> children;
public ObservableCollection<TreeViewFolderViewModel> Children
{
get
{
return children ?? (children =
new ObservableCollection<TreeViewFolderViewModel>());
}
set { children = value; OnPropertyChanged("Children"); }
}
}
public class TreeViewModel : ViewModelBase
{
private List<TreeViewFolderViewModel> items;
public List<TreeViewFolderViewModel> Items
{
get { return items; }
set { items = value; OnPropertyChanged("Items"); }
}
public TreeViewModel()
{
Items = new List<TreeViewFolderViewModel>()
{
new TreeViewFolderViewModel()
{
Id =0, Text="RootFolder", Children=new ObservableCollection<TreeViewFolderViewModel>()
{
new TreeViewFolderViewModel() { Id = 10, Text = "FirstFolder", Children=new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 11, Text = "FirstChild" } } } ,
new TreeViewFolderViewModel() { Id = 20, Text = "SecondFolder", Children = new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 21, Text = "SecondChild" } } } ,
new TreeViewFolderViewModel() { Id = 30, Text = "ThirdFolder", Children = new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 31, Text = "ThirdChild" } } }
}
}
};
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TreeViewModel />
</Window.DataContext>
<Window.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"
DataType="{x:Type local:TreeViewFolderViewModel}">
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="Id" />
<Binding Path="Text" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Items}" />
</Grid>
</Window>
I am trying to add to the TreeView the ability to catch the IsExpanded event. So that when a certain Item will expand it will raise a property or command on the view model.
Here is my code:
<TreeView Name="ScenariosTreeView" ItemsSource="{Binding Path=Cat, Mode=TwoWay}" Focusable="True" >
<TreeView.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedValue ,ElementName=ScenariosTreeView}" />
</TreeView.InputBindings>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExtended, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type sotc:ScenarioCategory}" ItemsSource="{Binding Path=ScenarioList}" >
<TextBlock Text="{Binding Path=Name}" Foreground="Black" IsEnabled="True" Focusable="True"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type sotc:Scenario}" >
<TextBlock Text="{Binding Path=Name, Mode=TwoWay}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
I also have a property in the viewmodel for IsExpanded (and for IsSelected) and none of that raises when I am expand the TreeviewItem.
Any Ideas?
Thanks ahead,
Tried to reproduce the issue and implemented attached behavior for calling command when node is expanded and everything works fine. Complete code for the solution:
XAML
<Window x:Class="TreeViewTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sotc="clr-namespace:TreeViewTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<sotc:MainWindowViewModel />
</Window.DataContext>
<Grid>
<TreeView Name="ScenariosTreeView" ItemsSource="{Binding Path=Cat, Mode=TwoWay}" Focusable="True" >
<TreeView.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedValue ,ElementName=ScenariosTreeView}" />
</TreeView.InputBindings>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExtended, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="sotc:Behaviours.ExpandingBehaviour" Value="{Binding DataContext.ExpandingCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type sotc:ScenarioCategory}" ItemsSource="{Binding Path=ScenarioList}" >
<TextBlock Text="{Binding Path=Name}" Foreground="Black" IsEnabled="True" Focusable="True"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type sotc:Scenario}" >
<TextBlock Text="{Binding Path=Name, Mode=TwoWay}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
C#
public class ViewModelBase : INotifyPropertyChanged
{
public void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class MainWindowViewModel : ViewModelBase
{
public ICommand ExpandingCommand { get; set; }
private void ExecuteExpandingCommand(object obj)
{
Console.WriteLine(#"Expanded");
}
private bool CanExecuteExpandingCommand(object obj)
{
return true;
}
public MainWindowViewModel()
{
ExpandingCommand = new RelayCommand(ExecuteExpandingCommand, CanExecuteExpandingCommand);
Cat = new ObservableCollection<ScenarioCategory>(new ScenarioCategory[]
{
new ScenarioCategory { Name = "C1" }, new ScenarioCategory { Name = "C2" }, new ScenarioCategory { Name = "C3" }
});
}
public ObservableCollection<ScenarioCategory> Cat { get; set; }
}
public class ScenarioCategory : ViewModelBase
{
string _name;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
bool _isExtended;
public bool IsExtended
{
get { return _isExtended; }
set
{
_isExtended = value;
Console.WriteLine(#"IsExtended set");
OnPropertyChanged();
}
}
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
Console.WriteLine(#"IsSelected set");
OnPropertyChanged();
}
}
public ObservableCollection<Scenario> ScenarioList { get; set; }
public ScenarioCategory()
{
ScenarioList = new ObservableCollection<Scenario>(new Scenario[] { new Scenario { Name = "1" }, new Scenario { Name = "2" }, new Scenario { Name = "3" } });
}
}
public class Scenario : ViewModelBase
{
string _name;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
bool _isExtended;
public bool IsExtended
{
get { return _isExtended; }
set
{
_isExtended = value;
OnPropertyChanged();
}
}
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
public static class Behaviours
{
public static readonly DependencyProperty ExpandingBehaviourProperty =
DependencyProperty.RegisterAttached("ExpandingBehaviour", typeof(ICommand), typeof(Behaviours),
new PropertyMetadata(OnExpandingBehaviourChanged));
public static void SetExpandingBehaviour(DependencyObject o, ICommand value)
{
o.SetValue(ExpandingBehaviourProperty, value);
}
public static ICommand GetExpandingBehaviour(DependencyObject o)
{
return (ICommand)o.GetValue(ExpandingBehaviourProperty);
}
private static void OnExpandingBehaviourChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeViewItem tvi = d as TreeViewItem;
if (tvi != null)
{
ICommand ic = e.NewValue as ICommand;
if (ic != null)
{
tvi.Expanded += (s, a) =>
{
if (ic.CanExecute(a))
{
ic.Execute(a);
}
a.Handled = true;
};
}
}
}
}
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute == null || this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
}
Found the answer. It fired the event if it is placed in the object represented as a treeviewItem and not in the view model.