the problem I have is following: when first two rows in DataGrid are selected and the first row is deleted, the selected row below becomes the first row and gets deselected. If I sort any of the columns, the selection of that row comes back. Or, if I close the window I get information that deselected row is actually selected (querying content of underlying bound property SelectedUsers in UsersViewModel - done in OnClosing method). Is there anyone who can help or explain me if I did something wrong or this might be a bug. I provided complete source code below. Thanks for help.
MainWindow.xaml
<Window x:Class="DeleteFirstRowIssue.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DeleteFirstRowIssue"
Title="MainWindow" Height="350" Width="400">
<Window.Resources>
<Style x:Key="CustomDataGridCellStyle" TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="*"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Users:" Grid.Row="0" Grid.Column="0"/>
<local:CustomDataGrid x:Name="UsersDataGrid" ItemsSource="{Binding UsersViewSource.View}" SelectionMode="Extended" AlternatingRowBackground="LightBlue" AlternationCount="2"
SelectionUnit="FullRow" IsReadOnly="True" SnapsToDevicePixels="True" AutoGenerateColumns="False" Grid.Row="1" Grid.Column="0" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False"
SelectedItemsList="{Binding SelectedUsers, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="False" CellStyle="{StaticResource CustomDataGridCellStyle}">
<local:CustomDataGrid.Columns>
<DataGridTextColumn Header="Nickname:" Width="*" Binding="{Binding Nickname}"/>
<DataGridTextColumn Header="Age:" Width="*" Binding="{Binding Age}"/>
</local:CustomDataGrid.Columns>
</local:CustomDataGrid>
<Button Grid.Row="2" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete 1st row" Command="{Binding DeleteFirstUserCommand}"/>
<Button Grid.Row="3" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Delete last row" Command="{Binding DeleteLastUserCommand}"/>
<Button Grid.Row="4" Grid.Column="0" Margin="5" MaxWidth="80" MinWidth="80" Content="Initialize Grid" Command="{Binding InitializeListCommand}"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace DeleteFirstRowIssue
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new UsersViewModel();
}
protected override void OnClosing(CancelEventArgs e)
{
UsersViewModel uvm = (UsersViewModel)DataContext;
if (uvm.SelectedUsers.Count > 0)
{
StringBuilder sb = new StringBuilder(uvm.SelectedUsers.Count.ToString() + " selected user(s):\n");
foreach (UserModel um in uvm.SelectedUsers)
{
sb.Append(um.Nickname + "\n");
}
MessageBox.Show(sb.ToString());
}
base.OnClosing(e);
}
}
public class UsersViewModel : INotifyPropertyChanged
{
private IList selectedUsers;
public IList SelectedUsers
{
get { return selectedUsers; }
set
{
selectedUsers = value;
OnPropertyChanged("SelectedUsers");
}
}
public CollectionViewSource UsersViewSource { get; private set; }
public ObservableCollection<UserModel> Users { get; set; }
public ICommand DeleteFirstUserCommand { get; }
public ICommand DeleteLastUserCommand { get; }
public ICommand InitializeListCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
public UsersViewModel()
{
SelectedUsers = new ArrayList();
Users = new ObservableCollection<UserModel>();
UsersViewSource = new CollectionViewSource() { Source = Users };
InitializeListCommand = new RelayCommand(p => Users.Count == 0, p => InitializeList());
InitializeListCommand.Execute(null);
DeleteFirstUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteFirstUser());
DeleteLastUserCommand = new RelayCommand(p => Users.Count > 0, p => DeleteLastUser());
}
private void InitializeList()
{
Users.Add(new UserModel() { Nickname = "John", Age = 35 });
Users.Add(new UserModel() { Nickname = "Jane", Age = 29 });
Users.Add(new UserModel() { Nickname = "Mark", Age = 59 });
Users.Add(new UserModel() { Nickname = "Susan", Age = 79 });
Users.Add(new UserModel() { Nickname = "Joe", Age = 66 });
Users.Add(new UserModel() { Nickname = "Nina", Age = 29 });
Users.Add(new UserModel() { Nickname = "Selma", Age = 44 });
Users.Add(new UserModel() { Nickname = "Katrin", Age = 24 });
Users.Add(new UserModel() { Nickname = "Joel", Age = 32 });
}
private void DeleteFirstUser()
{
ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
lcw.RemoveAt(0);
}
private void DeleteLastUser()
{
ListCollectionView lcw = (ListCollectionView)UsersViewSource.View;
lcw.RemoveAt(lcw.Count - 1);
}
}
public class UserModel
{
public string Nickname { get; set; }
public int Age { get; set; }
}
public class CustomDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomDataGrid), new PropertyMetadata(null));
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public CustomDataGrid() { SelectionChanged += CustomDataGrid_SelectionChanged; }
void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { SelectedItemsList = SelectedItems; }
}
public class RelayCommand : ICommand
{
private Predicate<object> canExecute;
private Action<object> execute;
public RelayCommand(Predicate<object> canExecute, Action<object> execute)
{
this.canExecute = canExecute;
this.execute = execute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) { return canExecute(parameter); }
public void Execute(object parameter) { execute(parameter); }
}
}
I encountered a similar issue implementing SelectedItemsList. The main gotcha is that when the selection gets renewed, the backing list tries to delete the items that are no longer in the selection. If, however, an item is changed (or no longer exists) the comparison fails and the item remains in the list, causing this behaviour.
You can keep the list in sync by adding a CollectionChanged EventHandler.
public UsersViewModel()
{
...
Users.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (UserModel item in e.OldItems) SelectedUsers.Remove(item);
}
As commented, somehow when the first row is deleted, the new first rows IsSelected property becomes out of sync with its cells IsSelected property. I don't know why this happens, but it is indicating a workaround if you are mainly interested in keeping the style working: Just use the rows property in the trigger
<DataTrigger Binding="{Binding IsSelected,RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</DataTrigger>
Related
I have made a tool for database that is showing items with certain filtering options. However after reading some more about WPF and C#. I used this https://www.codeproject.com/Articles/683429/Guide-to-WPF-DataGrid-formatting-using-bindings tutorial to modify my application to deal with ItemCollectionViewSource
I have used this before for filtering:
public void FilterSetup()
{
try
{
string qry = null;
_conditions["name"] = null;
if (!string.IsNullOrEmpty(BusinessIDSearch.Text))
{
qry = string.Format("LY Like '{0}%'", BusinessIDSearch.Text);
}
if (!string.IsNullOrEmpty(NameSearch.Text))
{
if (!string.IsNullOrEmpty(qry))
qry += " AND ";
qry += string.Format("HAKUNIMI Like '%{0}%'", NameSearch.Text);
}
if (!string.IsNullOrEmpty(GroupSearch.Text))
{
if (!string.IsNullOrEmpty(qry))
qry += " AND ";
qry += string.Format("KONSERNI Like '{0}%'", GroupSearch.Text);
}
if (!string.IsNullOrEmpty(LiinosIDSearch.Text))
{
if (!string.IsNullOrEmpty(qry))
qry += " AND ";
qry += string.Format("YRNRO Like '{0}%'", LiinosIDSearch.Text);
}
_conditions["name"] = qry;
UpdateFilter();
//LiinosFICount.Content = DataGrid1.Items.Count;
}
catch (Exception)
{
throw;
}
}
Then this:
private void UpdateFilter()
{
try
{
var activeConditions = _conditions.Where(c => c.Value != null).Select(c => "(" + c.Value + ")");
DataView dv = DataGrid1.ItemsSource as DataView;
dv.RowFilter = string.Join(" AND ", activeConditions);
}
catch (Exception)
{
//MessageBox.Show(ex.Message);
}
}
However it looks like I need another approach now with ItemCollectionViewSource.
Here is XAML part for textbox:
<TextBox Style="{StaticResource TextBox_Style}" x:Name="GroupSearch" HorizontalAlignment="Left" Margin="414,121,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="90" Height="20" TextChanged="GroupSearch_TextChanged"/>
<TextBox Style="{StaticResource TextBox_Style}" x:Name="NameSearch" HorizontalAlignment="Left" Margin="130,121,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="279" Height="20" TextChanged="NameSearch_TextChanged"/>
<TextBox Style="{StaticResource TextBox_Style}" Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}" x:Name="LiinosIDSearch" HorizontalAlignment="Left" Margin="20,121,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="105" Height="20"/>
<TextBox Style="{StaticResource TextBox_Style}" x:Name="BusinessIDSearch" HorizontalAlignment="Left" Margin="509,121,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="192" Height="20" TextChanged="BusinessIDSearch_TextChanged"/>
Here is XAML for DataGrid:
<DataGrid Margin="0,146,0,0" Background="{x:Null}" BorderBrush="{x:Null}"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserResizeRows="False" IsReadOnly="True"
HorizontalGridLinesBrush="#FF377A6C" VerticalGridLinesBrush="#FF377A6C"
DataContext="{StaticResource ItemCollectionViewSource}"
ItemsSource="{Binding}"
AutoGenerateColumns="False" FontFamily="Arial Nova" Foreground="White" >
<DataGrid.RowStyle>
<Style TargetType="DataGridRow" >
<Setter Property="Background" Value="{Binding YRNRO, Converter={StaticResource LiinosIDToBackgroundConverter}}" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Resources>
<Style BasedOn="{StaticResource {x:Type DataGridColumnHeader}}" TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="#377A6C" />
<Setter Property="Foreground" Value="White" />
<Setter Property="MinHeight" Value="20" />
</Style>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="#5AC37E"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding YRNRO}">
<DataGridTextColumn.Header>
<TextBlock Text="LIINOS ID" FontWeight="Bold" TextAlignment="Left"/>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding HAKUNIMI}">
<DataGridTextColumn.Header>
<TextBlock Text="SEARCH NAME" FontWeight="Bold" TextAlignment="Left"/>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding KONSERNI}">
<DataGridTextColumn.Header>
<TextBlock Text="GROUP" FontWeight="Bold" TextAlignment="Left"/>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding LY}">
<DataGridTextColumn.Header>
<TextBlock Text="BUSINESS ID" FontWeight="Bold" TextAlignment="Left"/>
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
I have tried several tutorials without success. Does anybody have any kind of tutorial to suggest or maybe provide an answer with solution I can implement to other TextBoxes as well? The tricky part that I have several TextBoxes and I need to combine filter query based on values in TextBoxes.
As you can see I have tried to apply Binding here:
<TextBox Style="{StaticResource TextBox_Style}" Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}" x:Name="LiinosIDSearch" HorizontalAlignment="Left" Margin="20,121,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="105" Height="20"/>
I have got this from some tutorial and tried to modify it to my needs without success:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows.Data;
namespace Liinos_inspector_FilterTest
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _filter1 = "";
private string _filter2 = "";
private string _filter3 = "";
public ViewModel()
{
var List = MainProcess.CustomersInLiinos.AsEnumerable()
.GroupBy(x => x.Field<string>("HAKUNIMI"))
.Where(x => x.Count() > 1)
.SelectMany(x => x)
.ToList();
//ItemList = new ObservableCollection<Items>(List);
ItemView = (CollectionView)CollectionViewSource.GetDefaultView(List);
ItemView.Filter = TextFilter;
}
private bool TextFilter(object obj)
{
var data = obj as Items;
if (data != null)
{
return data.Text1.StartsWith(_filter1) && data.Text2.StartsWith(_filter2) && data.Text3.StartsWith(_filter3);
}
return false;
}
private void NotifyPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public ObservableCollection<Items> ItemList { get; set; }
public CollectionView ItemView { get; set; }
public string Filter1
{
get { return _filter1; }
set
{
_filter1 = value;
NotifyPropertyChanged("Filter1");
ItemView.Refresh();
}
}
public string Filter2
{
get { return _filter2; }
set
{
_filter2 = value;
NotifyPropertyChanged("Filter2");
ItemView.Refresh();
}
}
public string Filter3
{
get { return _filter3; }
set
{
_filter3 = value;
NotifyPropertyChanged("Filter3");
ItemView.Refresh();
}
}
}
public class Items
{
public string Text1 { get; set; }
public string Text2 { get; set; }
public string Text3 { get; set; }
}
}
EDIT:
I am loading data currently on a button click to DataGrid:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
if (MainProcess.CheckForVPNInterface() == true)
{
if (MainProcess.Customers != null)
{
ProgressBar.IsIndeterminate = true;
CollectionViewSource itemCollectionViewSource;
itemCollectionViewSource = (CollectionViewSource)(FindResource("ItemCollectionViewSource"));
itemCollectionViewSource.Source = await LoadMainTableDataAsync();
ProgressBar.IsIndeterminate = false;
}
}
else
{
string caption = "VPN connection missing";
MessageBox.Show("Please, check your VPN connection!", caption,
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
}
}
EDIT 2:
Here is LoadMainTableDataAsync method that I have been using:
public Task<DataView> LoadMainTableDataAsync()
{
return Task.Run(() =>
{
MainProcess.MergedTable();
return MainProcess.Customers.DefaultView;
});
}
Here is merging of DataTables:
public static DataTable Customers = new DataTable();
public static void MergedTable()
{
var t1 = ConnectAndRetriveDatatatableS(); // t1
var t2 = ConnectAndRetriveDatatatableF(); // t2
Customers = t1.Copy();
Customers.Merge(t2);
}
You don't need a CollectionViewSource to filter the collection. Simply bind to a collection of your view model. In WPF collections are consumed via a ICollectionsView under the hoods. Each collection returns a default view. Controls are operating on this view automatically. To change the order, grouping or filtering you modify the current view of the collection instead of the actual collection instance.
Also don't set the DataContext explicitly to the CollectionViewSource. Bind directly to it.
As a side note, since you are binding to the model items, Items should implement INotifyPropertyChanged or you provoke memory leaks.
Don't handle the ProgressBar in your view model. Set a bool property and bind it to the ProgressBar.Visibility.
Don't show a MessageBox or any user dialog from the view model in order to handle an exception. Rather throw a meaning full custom exception or wrapper exception (with the original exceptions as inner exception) and handle it in your view e.g by showing an interaction dialog.
Don't handle database or any other data (model) access in your view. Rather implement an ICommand and assign it to the button to replace the event handler, and execute the operations in your view model. See Microsoft Docs: Relaying Command Logic for a simple implementation of RelayCommand.
This is a basic example how to filter a collection using the collection's default ICollectionView, incorporating the suggested improvements. You need to adjust the actual filter logic to your requirements:
VpnInterfaceException.cs
class VpnInterfaceException : Exception
{
public VpnInterfaceException(string message) : base(message)
{
}
public VpnInterfaceException(string message, Exception innerException) : base(message, innerException)
{
}
}
Item.cs
class Item : INotifyPropertyChanged
{
private string text;
public string Text
{
get => this.text;
set
{
this.text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Item> Items { get; set; }
public ICommand LoadMainTableDataCommand => new RelayCommand(ExecuteLoadMainTableDataAsync);
// Binding source for the TextBox
private string searchKey;
public string SearchKey
{
get => this.searchKey;
set
{
this.searchKey = value;
OnPropertyChanged();
// Refresh the ICollectionView to update the filter expression
CollectionViewSource.GetDefaultView(this.Items).Refresh();
}
}
private bool hasProgress;
public bool HasProgress
{
get => this.hasProgress;
set
{
this.hasProgress = value;
OnPropertyChanged();
}
}
public ViewModel()
{
this.Items = new ObservableCollection<Item>();
EnableItemsFiltering();
}
public void EnableItemsFiltering()
{
// Assign the filter expression which is executed when items are added
// or the 'ICollectionView.Refresh()' was called
CollectionViewSource.GetDefaultView(this.Items).Filter = FilterPredicate;
}
// The filter expression.
// Returns 'true' to include the current item.
private bool FilterPredicate(object item)
=> string.IsNullOrWhiteSpace(this.SearchKey)
|| ((item as Item)?.Text.StartsWith(this.SearchKey, StringComparison.OrdinalIgnoreCase) ?? false);
private async Task ExecuteLoadMainTableDataAsync(object commandParameter)
{
if (MainProcess.CheckForVPNInterface())
{
if (MainProcess.Customers != null)
{
this.HasProgress = true;
IEnumerable<Item> resultItems = await LoadMainTableDataAsync();
this.Items = new ObservableCollection<Item>(resultItems);
EnableItemsFiltering();
this.HasProgress = false;
}
}
else
{
// Throw an exception since the operation cannot be completed unexpectedly.
// The view can catch this exception to execute the error handling.
throw new VpnInterfaceException("Please, check your VPN connection!");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Handle specific view model exceptions for user interaction
this.Dispatcher.UnhandledException += (sender, args) =>
{
// Only handle selected exceptions, that can be handled through user interaction
if (args.Exception is VpnInterfaceException exception)
{
args.Handled = true;
string caption = "VPN connection missing";
// Convert exception message to user friendly message
MessageBox.Show(exception.Message,
caption,
MessageBoxButton.OK,
MessageBoxImage.Exclamation);
}
};
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<StackPanel>
<ProgressBar IsIndeterminate="True"
Visibility="{Binding HasProgress, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Command="{Binding LoadMainTableDataCommand}"
Content="Load Data" />
<TextBox Text="{Binding SearchKey}" />
<DataGrid ItemsSource="{Binding Items}" />
<StackPanel>
</Window>
I have a view model that presents items in ListBox. There are two collection: Source contains all element and Checked contains only checked elements. There are two buttons SelectAll and ClearAll. When I push on one of this button View Model works well and both Source and Checked collections updating, but no changes in Listbox happens.
CheckItemPresenterVM<T> - an element that save the state of one button and implements INotifyPropertyChange, but when property IsChecked changes CollectionChangedevent doesn't reise.
The question is how to make it work?
<UserControl.Resources>
<ItemsPanelTemplate x:Key="listPanelTemplate">
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
<Style TargetType="ListBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ListBoxItemIsCheckedBinding"
TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected"
Value="{Binding IsChecked, Mode=TwoWay}" />
<Setter Property="Padding"
Value="5,3" />
<Setter Property="Background"
Value="LightCyan"></Setter>
</Style>
<Style TargetType="Button">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
<Style TargetType="ToggleButton">
<Setter Property="Padding"
Value="7, 3" />
<Setter Property="Margin"
Value="1" />
</Style>
</UserControl.Resources>
<Grid>
<Expander Header="Categories"
IsExpanded="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WrapPanel Grid.Row="0"
Margin="0, 5">
<Button Command="{Binding SelectAll}">Select All</Button>
<Button Command="{Binding ClearAll}">Clear All</Button>
<Button Command="{Binding InvertSelection}">Invert Selection</Button>
</WrapPanel>
<ListBox Grid.Row="1" />
<ListBox Name="CategoriesListBox"
Grid.Row="2"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Source, Mode=TwoWay}"
ItemContainerStyle="{DynamicResource ListBoxItemIsCheckedBinding}"
ItemsPanel="{StaticResource listPanelTemplate}" />
<ListBox Name="CheckedListBox"
Grid.Row="3"
SelectionMode="Multiple"
ItemsSource="{Binding CategoryVM.Checked}"/>
</Grid>
</Expander>
</Grid>
class TestViewModel
{
public CheckItemVM<string> CategoryVM { get; set; }
public TestViewModel()
{
logger.FileName = "TestSelectionFilter.txt";
logger.AddEventRecord(this);
CategoryVM = new CheckItemVM<string>(
new List<string>() {
"01 elem",
"02 elem",
"03 elem",
"04 elem",
"05 elem",
"06 elem",
"07 elem",
"08 elem",
"09 elem",
"10 elem",
"11 elem",
"12 elem",
"13 elem",
"14 elem",
"15 elem",
"16 elem",
"17 elem",
"18 elem",
"19 elem",
"20 elem",
"21 elem",
"22 elem",
"23 elem",
"24 elem",
"25 elem"});
}
public ICommand SelectAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.SelectAll();
},
CanExecutePredicate = p =>
{
return !CategoryVM.IsAllSelected();
}
};
}
}
public ICommand InvertSelection
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.InvertSelection();
},
CanExecutePredicate = p =>
{
return true;
}
};
}
}
public ICommand ClearAll
{
get
{
return new RelayCommand
{
ExecuteAction = a =>
{
CategoryVM.ClearAll();
},
CanExecutePredicate = p =>
{
return CategoryVM.Checked.Any();
}
};
}
}
class CheckItemVM<T>
{
protected ObservableCollection<CheckItemPresenterVM<T>> _checked = new ObservableCollection<CheckItemPresenterVM<T>>();
public ObservableCollection<CheckItemPresenterVM<T>> Source { get; set; }
public ObservableCollection<CheckItemPresenterVM<T>> Checked { get => _checked; }
public CheckItemVM(ICollection<T> _source)
{
Source = new ObservableCollection<CheckItemPresenterVM<T>>();
UpdateSource(_source);
}
protected void UpdateSource(ICollection<T> _source)
{
foreach (var item in _source)
{
Source.Add(new CheckItemPresenterVM<T>(ref _checked)
{ Item = item, IsChecked = false });
}
}
public void SetSelection(CheckItemPresenterVM<T> sourceItem, bool flag)
{
if (sourceItem.IsChecked != flag)
{
sourceItem.IsChecked = flag;
}
}
public void SelectAll()
{
foreach (var item in Source)
{
item.IsChecked = true;
}
}
public void ClearAll()
{
foreach (var item in Source)
{
item.IsChecked = false;
}
}
public void InvertSelection()
{
foreach (var item in Source)
{
if (item.IsChecked) item.IsChecked = false;
else item.IsChecked = true;
}
}
public bool IsAllSelected()
{
return Source.Count == Checked.Count;
}
}
class CheckItemPresenterVM<T> : INotifyPropertyChanged
{
protected bool _isChecked;
protected ObservableCollection<CheckItemPresenterVM<T>> _checked;
public CheckItemPresenterVM(ref ObservableCollection<CheckItemPresenterVM<T>> Checked)
{
_checked = Checked;
}
public T Item { get; set; }
public string Name { get; set; }
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("Item was Checked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("Item Unchecked");
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public override string ToString()
{
return Item.ToString();
}
}
I see strange part of you code.
Guessing this fix in PropertyChanged raising call
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
if (value)
{
if (!_checked.Contains(this))
{
_checked.Add(this);
NotifyPropertyChanged("IsChecked");
}
}
else
{
if (_checked.Contains(this))
{
_checked.Remove(this);
NotifyPropertyChanged("IsChecked");
}
}
}
}
NotifyPropertyChanged fires PoropertyChanged Event with a string parameter you passing to. Meanwhile Binding will receive it.
So, here's 2 friends
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay}" />
and
NotifyPropertyChanged("IsChecked");
If you need tune the update behavior of your control, pass UpdateSourceTrigger setting to to the binding, this way:
<Setter Property="IsSelected" Value="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Additionally: TwoWay is default for the Mode here. You may not declare it.
When binding a View/Control to an ObservableCollection the View/Control will be redrawn when the CollectionChanged event of ObservableCollection is raised.
This occurs when an item is added, removed, reordered or assigned a new instance; but not (as you've probably realised) when an item raises it's PropertyChanged event.
To get your changes to be reflected in the UI you need the PropertyChanged of each Item to raise the CollectionChanged event of the collection.
public class ObservableItemCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
// Call this from the constructor
private void InitialiseItems()
{
CollectionChanged += ContentCollectionChanged;
foreach (T item in Items)
item.PropertyChanged += ReplaceElementWithItself;
}
private void ContentCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (T item in e.OldItems)
{
item.PropertyChanged -= ReplaceElementWithItself;
}
}
if (e.NewItems != null)
{
foreach (T item in e.NewItems)
{
item.PropertyChanged += ReplaceElementWithItself;
}
}
}
private void ReplaceElementWithItself(object sender, PropertyChangedEventArgs e)
{
var collectionChangedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
// Call this on the main thread
OnCollectionChanged(collectionChangedArgs);
}
}
This implementation simply raises the CollectionChanged event as a 'Replace' where the same Item is both removed and inserted, at the same index. Beware that it could have unintended consequences if you have anything that actually cares about the CollectionChanged events beyond binding it to the UI; but it is the simplest way I have found to achieve what you are after.
I have a DataGrid and a ListView in my application. The ListView provides information about the datagrid's selected item. I put a HyperLink in that ListView that should change the datagrid's selected item to the "parent" of the currently selected item.
My approach is to set SelectedItemin my code behind. Everything works quite well but the datagrid doesn't highlight the new selected item. But I can clearly see it is selected because off its gray background color. Is it possible to set the highlighted cell programmatically?
<ListView>
<TextBlock Text="{Binding SelectedParameter.Definition.Name, StringFormat=Name: {0:C}}"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding SelectedParameter.Definition.Type, StringFormat=Datentyp: {0:C}}"
TextWrapping="Wrap"/>
TextWrapping="Wrap"/>
<Hyperlink Command="{Binding GoToMasterParameterCommand}">
Masterparameter
</Hyperlink>
</ListView>
<DataGrid Name="m_DataGrid"
ItemsSource="{Binding Path=Parameters}"
SelectedItem="{Binding SelectedParameter}"
SelectionMode="Single"
AutoGenerateColumns="False"
TargetUpdated="m_ParameterDataGrid_TargetUpdated">
<DataGrid.Columns>
<DataGridTextColumn
Header="ID"
Binding="{Binding Id}"
IsReadOnly="True"/>
<DataGridTextColumn
Header="Value"
Binding="{Binding Value.CurrentInternalValue, NotifyOnTargetUpdated=True}"
</DataGrid.Columns>
</DataGrid>
internal void GoToMasterParameter()
{
string parentId = GetParentId(this.SelectedParameter);
this.SelectedParameter = this.Parameters.Single(item => item.Id == parentId);
}
The problem you have is that the selected rows/cells are not focused as the focus is still in your listView item. What you could do is style the DataGridCell element in xaml. Here is a small piece of code that demonstrate this:
<Window.Resources>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<DataGrid ItemsSource="{Binding Tests}"
SelectedItem="{Binding GridSelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedGridIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</DataGrid>
<Button Command="{Binding ChangeSelectedItemCommand}"
Content="Change Grid Selected item"
Grid.Column="1"
VerticalAlignment="Top"/>
</Grid>
and here the viewModel part:
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Private members
private List<TestClass> _tests;
private TestClass _gridSelectedItem;
private ICommand _changeSelectedItemCommand;
private int _selectedGridIndex;
#endregion
#region Constructor
public MainWindowViewModel()
{
Tests = new List<TestClass>();
for (int i = 0; i < 25; i++)
{
TestClass testClass= new TestClass {Name = "Name " + i, Title = "Title" + i};
Tests.Add(testClass);
}
}
#endregion
#region Public properties
public List<TestClass> Tests
{
get { return _tests; }
set
{
_tests = value;
OnPropertyChanged("Tests");
}
}
public TestClass GridSelectedItem
{
get { return _gridSelectedItem; }
set
{
_gridSelectedItem = value;
OnPropertyChanged("GridSelectedItem");
}
}
public int SelectedGridIndex
{
get { return _selectedGridIndex; }
set
{
_selectedGridIndex = value;
OnPropertyChanged("SelectedGridIndex");
}
}
#endregion
public ICommand ChangeSelectedItemCommand
{
get { return _changeSelectedItemCommand ?? (_changeSelectedItemCommand = new SimpleCommand(p => ChangeSelectedGridItem())); }
}
private void ChangeSelectedGridItem()
{
SelectedGridIndex++;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
the demo objects class:
public class TestClass
{
public string Title { get; set; }
public string Name { get; set; }
}
and some command class:
public class SimpleCommand : ICommand
{
private readonly Predicate<object> _canExecuteDelegate;
private readonly Action<object> _executeDelegate;
#region Constructors
public SimpleCommand(Action<object> execute)
: this(execute, null)
{
}
public SimpleCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
_executeDelegate = execute;
_canExecuteDelegate = canExecute;
}
#endregion // Constructors
#region ICommand Members
public virtual bool CanExecute(object parameter)
{
return _canExecuteDelegate == null || _canExecuteDelegate(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_executeDelegate(parameter);
}
#endregion
}
Make sure to add the DataContext of the View so that it knows about your ViewModel:
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
I hope this helps you obtain the desired effect.
I have created a ListBox with custom ListBoxItem Template, had bound everything and it worked. When I was still working on my project. I runned the program and that ListBox weren't showing items anymore. Here is my code:
This is ListBox which doesn't show any item:
<ListBox
x:Name="LB_SongList"
HorizontalAlignment="Left"
Height="498"
Margin="0,30,0,0"
VerticalAlignment="Top"
Width="319"
Background="{x:Null}"
BorderBrush="{x:Null}"
Drop="LB_SongList_Drop"
PreviewMouseMove="LB_SongList_PreviewMouseMove"
PreviewMouseDoubleClick="LB_SongList_PreviewMouseDoubleClick"
SelectionChanged="LB_SongList_SelectionChanged"
AllowDrop="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible"
SelectionMode="Multiple"
ItemTemplate="{DynamicResource SongTemplate}"/>
Here is my ListBoxItem Template:
<DataTemplate x:Key="SongTemplate">
<Grid Width="302" Height="35">
<Label Content="{Binding SongName}" HorizontalAlignment="Stretch" Margin="4" VerticalAlignment="Stretch" Width="Auto" Height="Auto" Padding="2,0,0,0" VerticalContentAlignment="Center"/>
<Border BorderBrush="#B26A6A6A" BorderThickness="4" HorizontalAlignment="Stretch" Height="35" VerticalAlignment="Top" Width="Auto">
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
Value="False">
<Setter
Property="Border.Visibility"
Value="Collapsed"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</Grid>
</DataTemplate>
And the code:
public partial class MenuHolder : UserControl
{
private List<string> List_SongList;
public List<string> L_SongList
{
get { return List_SongList; }
set
{
_File.ListToObservableCollection_Song(value, O_SongList);
List_SongList = value;
}
}
public List<string> L_PlayLists
{
get
{
Settings.Default.Save();
return Settings.Default.L_PlayLists;
}
set
{
Settings.Default.L_PlayLists = value;
Settings.Default.Save();
}
}
public ObservableCollection<Song> O_SongList = new ObservableCollection<Song>();
public ObservableCollection<string> O_PlayList = new ObservableCollection<string>();
public MenuHolder()
{
InitializeComponent();
LB_SongList.ItemsSource = O_SongList;
LB_PlayList.ItemsSource = O_PlayList;
List<string> temp = new List<string>();
temp.Add("TestSong");
L_SongList = temp;
}
}
public class Song : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _songName;
string _songLengh;
public Song(){}
public Song(String _name){ SongName = _name; }
public Song(String _name, String _lengh) { SongName = _name; SongLengh = _lengh; }
public string SongName
{
get { return _songName; }
set { _songName = value; RaisePropertyChanged("SongName"); }
}
public string SongLengh
{
get { return _songLengh; }
set { _songLengh = value; RaisePropertyChanged("SongLengh"); }
}
private void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the ListToObservableCollection_Song
public void ListToObservableCollection_Song(List<string> _list, ObservableCollection<Music_Player.Menus.MainMenuObjects.Song> _collection)
{
_collection.Clear();
foreach (string _path in _list)
{
_collection.Add(new Menus.MainMenuObjects.Song(GetSongNameFromPath(_path)));
}
}
The worst thing is that it was working before and in the old version of my program it is still working. Still thinking why it is doing that.
I have 3 TextBoxes (Id1,Name and Salary). Id and Salary should contain integers and Name should only contain characters. I need validations for my TextBox, it should show errors as I enter wrong characters or integers. Also can this be done only in Xaml without codebehind? Please help me with the required code
This is Xaml code:
<TextBox Name="tb1" HorizontalAlignment="Left" Height="20" Margin="60,10,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Id,ElementName=dgsample}" VerticalAlignment="Top" Width="100" />
<TextBox Name="tb2" HorizontalAlignment="Left" Height="20" Margin="60,60,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Name, ElementName=dgsample}" VerticalAlignment="Top" Width="100"/>
<TextBox Name="tb3" HorizontalAlignment="Left" Height="20" Margin="60,110,0,0" TextWrapping="NoWrap" Text="{Binding SelectedItem.Salary, ElementName=dgsample}" VerticalAlignment="Top" Width="100"/>
There a 3 ways to implement validation:
Validation Rule
Implementation of INotifyDataErrorInfo
Implementation of IDataErrorInfo
Validation rule example:
public class NumericValidationRule : ValidationRule
{
public Type ValidationType { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string strValue = Convert.ToString(value);
if (string.IsNullOrEmpty(strValue))
return new ValidationResult(false, $"Value cannot be coverted to string.");
bool canConvert = false;
switch (ValidationType.Name)
{
case "Boolean":
bool boolVal = false;
canConvert = bool.TryParse(strValue, out boolVal);
return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of boolean");
case "Int32":
int intVal = 0;
canConvert = int.TryParse(strValue, out intVal);
return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Int32");
case "Double":
double doubleVal = 0;
canConvert = double.TryParse(strValue, out doubleVal);
return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Double");
case "Int64":
long longVal = 0;
canConvert = long.TryParse(strValue, out longVal);
return canConvert ? new ValidationResult(true, null) : new ValidationResult(false, $"Input should be type of Int64");
default:
throw new InvalidCastException($"{ValidationType.Name} is not supported");
}
}
}
XAML:
Very important: don't forget to set ValidatesOnTargetUpdated="True" it won't work without this definition.
<TextBox x:Name="Int32Holder"
IsReadOnly="{Binding IsChecked,ElementName=CheckBoxEditModeController,Converter={converters:BooleanInvertConverter}}"
Style="{StaticResource ValidationAwareTextBoxStyle}"
VerticalAlignment="Center">
<!--Text="{Binding Converter={cnv:TypeConverter}, ConverterParameter='Int32', Path=ValueToEdit.Value, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"-->
<TextBox.Text>
<Binding Path="Name"
Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged"
Converter="{cnv:TypeConverter}"
ConverterParameter="Int32"
ValidatesOnNotifyDataErrors="True"
ValidatesOnDataErrors="True"
NotifyOnValidationError="True">
<Binding.ValidationRules>
<validationRules:NumericValidationRule ValidationType="{x:Type system:Int32}"
ValidatesOnTargetUpdated="True" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<!--NumericValidationRule-->
</TextBox>
INotifyDataErrorInfo example:
public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
ValidateAsync();
}
#endregion
public virtual void OnLoaded()
{
}
#region INotifyDataErrorInfo
private ConcurrentDictionary<string, List<string>> _errors = new ConcurrentDictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void OnErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
List<string> errorsForName;
_errors.TryGetValue(propertyName, out errorsForName);
return errorsForName;
}
public bool HasErrors
{
get { return _errors.Any(kv => kv.Value != null && kv.Value.Count > 0); }
}
public Task ValidateAsync()
{
return Task.Run(() => Validate());
}
private object _lock = new object();
public void Validate()
{
lock (_lock)
{
var validationContext = new ValidationContext(this, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(this, validationContext, validationResults, true);
foreach (var kv in _errors.ToList())
{
if (validationResults.All(r => r.MemberNames.All(m => m != kv.Key)))
{
List<string> outLi;
_errors.TryRemove(kv.Key, out outLi);
OnErrorsChanged(kv.Key);
}
}
var q = from r in validationResults
from m in r.MemberNames
group r by m into g
select g;
foreach (var prop in q)
{
var messages = prop.Select(r => r.ErrorMessage).ToList();
if (_errors.ContainsKey(prop.Key))
{
List<string> outLi;
_errors.TryRemove(prop.Key, out outLi);
}
_errors.TryAdd(prop.Key, messages);
OnErrorsChanged(prop.Key);
}
}
}
#endregion
}
View Model Implementation:
public class MainFeedViewModel : BaseViewModel//, IDataErrorInfo
{
private ObservableCollection<FeedItemViewModel> _feedItems;
[XmlIgnore]
public ObservableCollection<FeedItemViewModel> FeedItems
{
get
{
return _feedItems;
}
set
{
_feedItems = value;
OnPropertyChanged("FeedItems");
}
}
[XmlIgnore]
public ObservableCollection<FeedItemViewModel> FilteredFeedItems
{
get
{
if (SearchText == null) return _feedItems;
return new ObservableCollection<FeedItemViewModel>(_feedItems.Where(x => x.Title.ToUpper().Contains(SearchText.ToUpper())));
}
}
private string _title;
[Required]
[StringLength(20)]
//[CustomNameValidationRegularExpression(5, 20)]
[CustomNameValidationAttribute(3, 20)]
public string Title
{
get { return _title; }
set
{
_title = value;
OnPropertyChanged("Title");
}
}
private string _url;
[Required]
[StringLength(200)]
[Url]
//[CustomValidation(typeof(MainFeedViewModel), "UrlValidation")]
/// <summary>
/// Validation of URL should be with custom method like the one that implemented below, or with
/// </summary>
public string Url
{
get { return _url; }
set
{
_url = value;
OnPropertyChanged("Url");
}
}
public MainFeedViewModel(string url, string title)
{
Title = title;
Url = url;
}
/// <summary>
///
/// </summary>
public MainFeedViewModel()
{
}
public MainFeedViewModel(ObservableCollection<FeedItemViewModel> feeds)
{
_feedItems = feeds;
}
private string _searchText;
[XmlIgnore]
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
OnPropertyChanged("SearchText");
OnPropertyChanged("FilteredFeedItems");
}
}
#region Data validation local
/// <summary>
/// Custom URL validation method
/// </summary>
/// <param name="obj"></param>
/// <param name="context"></param>
/// <returns></returns>
public static ValidationResult UrlValidation(object obj, ValidationContext context)
{
var vm = (MainFeedViewModel)context.ObjectInstance;
if (!Uri.IsWellFormedUriString(vm.Url, UriKind.Absolute))
{
return new ValidationResult("URL should be in valid format", new List<string> { "Url" });
}
return ValidationResult.Success;
}
#endregion
}
XAML:
<UserControl x:Class="RssReaderTool.Views.AddNewFeedDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<FrameworkElement.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate x:Name="TextErrorTemplate">
<DockPanel LastChildFill="True">
<AdornedElementPlaceholder>
<Border BorderBrush="Red"
BorderThickness="2" />
</AdornedElementPlaceholder>
<TextBlock FontSize="20"
Foreground="Red">*?*</TextBlock>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError"
Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource=
{x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"></Setter>
</Trigger>
</Style.Triggers>
</Style>
<!--<Style TargetType="{x:Type 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>-->
</FrameworkElement.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="Feed Name"
ToolTip="Display" />
<TextBox Text="{Binding MainFeedViewModel.Title,UpdateSourceTrigger=PropertyChanged,ValidatesOnNotifyDataErrors=True,ValidatesOnDataErrors=True}"
Grid.Column="2" />
<TextBlock Text="Feed Url"
Grid.Row="2" />
<TextBox Text="{Binding MainFeedViewModel.Url,UpdateSourceTrigger=PropertyChanged,ValidatesOnNotifyDataErrors=True,ValidatesOnDataErrors=True}"
Grid.Column="2"
Grid.Row="2" />
</Grid>
</UserControl>
IDataErrorInfo:
View Model:
public class OperationViewModel : ViewModelBase, IDataErrorInfo
{
private const int ConstCodeMinValue = 1;
private readonly IEventAggregator _eventAggregator;
private OperationInfoDefinition _operation;
private readonly IEntityFilterer _contextFilterer;
private OperationDescriptionViewModel _description;
public long Code
{
get { return _operation.Code; }
set
{
if (SetProperty(value, _operation.Code, o => _operation.Code = o))
{
UpdateDescription();
}
}
}
public string Description
{
get { return _operation.Description; }
set
{
if (SetProperty(value, _operation.Description, o => _operation.Description = o))
{
UpdateDescription();
}
}
}
public string FriendlyName
{
get { return _operation.FriendlyName; }
set
{
if (SetProperty(value, _operation.FriendlyName, o => _operation.FriendlyName = o))
{
UpdateDescription();
}
}
}
public int Timeout
{
get { return _operation.Timeout; }
set
{
if (SetProperty(value, _operation.Timeout, o => _operation.Timeout = o))
{
UpdateDescription();
}
}
}
public string Category
{
get { return _operation.Category; }
set
{
if (SetProperty(value, _operation.Category, o => _operation.Category = o))
{
UpdateDescription();
}
}
}
public bool IsManual
{
get { return _operation.IsManual; }
set
{
if (SetProperty(value, _operation.IsManual, o => _operation.IsManual = o))
{
UpdateDescription();
}
}
}
void UpdateDescription()
{
//some code
}
#region Validation
#region IDataErrorInfo
public ValidationResult Validate()
{
return ValidationService.Instance.ValidateNumber(Code, ConstCodeMinValue, long.MaxValue);
}
public string this[string columnName]
{
get
{
var validation = ValidationService.Instance.ValidateNumber(Code, ConstCodeMinValue, long.MaxValue);
return validation.IsValid ? null : validation.ErrorContent.ToString();
}
}
public string Error
{
get
{
var result = Validate();
return result.IsValid ? null : result.ErrorContent.ToString();
}
}
#endregion
#endregion
}
XAML:
<controls:NewDefinitionControl x:Class="DiagnosticsDashboard.EntityData.Operations.Views.NewOperationView"
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:views="clr-namespace:DiagnosticsDashboard.EntityData.Operations.Views"
xmlns:controls="clr-namespace:DiagnosticsDashboard.Core.Controls;assembly=DiagnosticsDashboard.Core"
xmlns:c="clr-namespace:DiagnosticsDashboard.Core.Validation;assembly=DiagnosticsDashboard.Core"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Column="0"
Grid.Row="0"
Margin="5">Code:</Label>
<Label Grid.Column="0"
Grid.Row="1"
Margin="5">Description:</Label>
<Label Grid.Column="0"
Grid.Row="2"
Margin="5">Category:</Label>
<Label Grid.Column="0"
Grid.Row="3"
Margin="5">Friendly Name:</Label>
<Label Grid.Column="0"
Grid.Row="4"
Margin="5">Timeout:</Label>
<Label Grid.Column="0"
Grid.Row="5"
Margin="5">Is Manual:</Label>
<TextBox Grid.Column="1"
Text="{Binding Code,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"
Grid.Row="0"
Margin="5"/>
<TextBox Grid.Column="1"
Grid.Row="1"
Margin="5"
Text="{Binding Description}" />
<TextBox Grid.Column="1"
Grid.Row="2"
Margin="5"
Text="{Binding Category}" />
<TextBox Grid.Column="1"
Grid.Row="3"
Margin="5"
Text="{Binding FriendlyName}" />
<TextBox Grid.Column="1"
Grid.Row="4"
Margin="5"
Text="{Binding Timeout}" />
<CheckBox Grid.Column="1"
Grid.Row="5"
Margin="5"
IsChecked="{Binding IsManual}"
VerticalAlignment="Center" />
</Grid>
</controls:NewDefinitionControl>
You can additionally implement IDataErrorInfo as follows in the view model. If you implement IDataErrorInfo, you can do the validation in that instead of the setter of a particular property, then whenever there is a error, return an error message so that the text box which has the error gets a red box around it, indicating an error.
class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private string m_Name = "Type Here";
public ViewModel()
{
}
public string Name
{
get
{
return m_Name;
}
set
{
if (m_Name != value)
{
m_Name = value;
OnPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public string Error
{
get { return "...."; }
}
/// <summary>
/// Will be called for each and every property when ever its value is changed
/// </summary>
/// <param name="columnName">Name of the property whose value is changed</param>
/// <returns></returns>
public string this[string columnName]
{
get
{
return Validate(columnName);
}
}
private string Validate(string propertyName)
{
// Return error message if there is error on else return empty or null string
string validationMessage = string.Empty;
switch (propertyName)
{
case "Name": // property name
// TODO: Check validiation condition
validationMessage = "Error";
break;
}
return validationMessage;
}
}
And you have to set ValidatesOnDataErrors=True in the XAML in order to invoke the methods of IDataErrorInfo as follows:
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
To get it done only with XAML you need to add Validation Rules for individual properties. But i would recommend you to go with code behind approach.
In your code, define your specifications in properties setters and throw exceptions when ever it doesn't compliance to your specifications.
And use error template to display your errors to user in UI.
Your XAML will look like this
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style x:Key="CustomTextBoxTextStyle" TargetType="TextBox">
<Setter Property="Foreground" Value="Green" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Trigger.Setters>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},Path=(Validation.Errors)[0].ErrorContent}"/>
<Setter Property="Background" Value="Red"/>
</Trigger.Setters>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<TextBox Name="tb2" Height="30" Width="400"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"
Style="{StaticResource CustomTextBoxTextStyle}"/>
</Grid>
Code Behind:
public partial class MainWindow : Window
{
private ExampleViewModel m_ViewModel;
public MainWindow()
{
InitializeComponent();
m_ViewModel = new ExampleViewModel();
DataContext = m_ViewModel;
}
}
public class ExampleViewModel : INotifyPropertyChanged
{
private string m_Name = "Type Here";
public ExampleViewModel()
{
}
public string Name
{
get
{
return m_Name;
}
set
{
if (String.IsNullOrEmpty(value))
{
throw new Exception("Name can not be empty.");
}
if (value.Length > 12)
{
throw new Exception("name can not be longer than 12 charectors");
}
if (m_Name != value)
{
m_Name = value;
OnPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I have implemented this validation. But you would be used code behind. It is too much easy and simplest way.
XAML:
For name Validtion only enter character from A-Z and a-z.
<TextBox x:Name="first_name_texbox" PreviewTextInput="first_name_texbox_PreviewTextInput" > </TextBox>
Code Behind.
private void first_name_texbox_PreviewTextInput ( object sender, TextCompositionEventArgs e )
{
Regex regex = new Regex ( "[^a-zA-Z]+" );
if ( regex.IsMatch ( first_name_texbox.Text ) )
{
MessageBox.Show("Invalid Input !");
}
}
For Salary and ID validation, replace regex constructor passed value with [0-9]+. It means you can only enter number from 1 to infinite.
You can also define length with [0-9]{1,4}. It means you can only enter less then or equal to 4 digit number. This baracket means {at least,How many number}. By doing this you can define range of numbers in textbox.
May it help to others.
XAML:
Code Behind.
private void salary_texbox_PreviewTextInput ( object sender, TextCompositionEventArgs e )
{
Regex regex = new Regex ( "[^0-9]+" );
if ( regex.IsMatch ( salary_texbox.Text ) )
{
MessageBox.Show("Invalid Input !");
}
}
When it comes to Muhammad Mehdi's answer, it is better to do:
private void salary_texbox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Regex regex = new Regex ( "[^0-9]+" );
if(regex.IsMatch(e.Text))
{
MessageBox.Show("Error");
}
}
Because when comparing with the TextCompositionEventArgs it gets also the last character, while with the textbox.Text it does not. With textbox, the error will show after next inserted character.
When I needed to do this, I followed Microsoft's example using Binding.ValidationRules and it worked first time.
See their article, How to: Implement Binding Validation:
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-binding-validation?view=netframeworkdesktop-4.8
When it comes to DiSaSteR's answer, I noticed a different behavior. textBox.Text shows the text in the TextBox as it was before the user entered a new character, while e.Text shows the single character the user just entered. One challenge is that this character might not get appended to the end, but it will be inserted at the carret position:
private void salary_texbox_PreviewTextInput(object sender, TextCompositionEventArgs e){
Regex regex = new Regex ( "[^0-9]+" );
string text;
if (textBox.CaretIndex==textBox.Text.Length) {
text = textBox.Text + e.Text;
} else {
text = textBox.Text.Substring(0, textBox.CaretIndex) + e.Text + textBox.Text.Substring(textBox.CaretIndex);
}
if(regex.IsMatch(text)){
MessageBox.Show("Error");
}
}