WPF Datagrid ValidationRules on ItemsSource - wpf

I want to put a red-border around a DataGrid when it has no rows (Im doing the binding to ItemsSource).
So i was following this guide for WPF Validation:
http://www.codeproject.com/Articles/15239/Validation-in-Windows-Presentation-Foundation
Anyhow, it is simple enough to make a textbox have such an error when the Text = "":
and even customize the error:
I tried debugging and the ValidationRules that are bound within my ItemsSource are never invoked.
<DataGrid ...>
<DataGrid.ItemsSource>
<Binding Path="Lines" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<DataGridValidationRule
MiniumRows="1"
MaximumRows="100"
ErrorMessage="must have between 1 and 100 rows">
</DataGridValidationRule>
</Binding.ValidationRules>
</Binding>
</DataGrid.ItemsSource>
</DataGrid>
And then the DataGridValidtionRule class looks like this:
public class public class StringRangeValidationRule : ValidationRule
{
private int _minimumRows = -1;
private int _maximumRows = -1;
private string _errorMessage;
public int MinimumRows
{
get { return _minimumRows ; }
set { _minimumRows = value; }
}
public int MaximumRows
{
get { return _maximumLength; }
set { _maximumLength = value; }
}
public string ErrorMessage
{
get { return _errorMessage; }
set { _errorMessage = value; }
}
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
ValidationResult result = new ValidationResult(true, null);
ObservableCollection<Lines> lines = (ObservableCollection<Lines>) value;
if (lines.Count < this.MinimumRows||
(this.MaximumRows> 0 &&
lines.Count > this.MaximumRows))
{
result = new ValidationResult(false, this.ErrorMessage);
}
return result;
}
And the "Lines" class
public class Line
{
public Line(string f, string b)
{
foo = f;
bar = b;
}
public string Foo {get; set;}
public string Bar {get; set;}
}
EDIT:
It turns out that my "delete row" button for my datagrid was removing from the ObservableCollection but not through the actual DataGrid (it was removing at the ViewModel)... and for some reason this prevents the Validation call from being invoked.
So again the View:
<DataGrid Name="mygrid">
<DataGrid.ItemsSource>
<Binding Path="Lines" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<DataGridValidationRule
MiniumRows="1"
MaximumRows="100"
ErrorMessage="must have between 1 and 100 rows">
</DataGridValidationRule>
</Binding.ValidationRules>
</Binding>
</DataGrid.ItemsSource>
</DataGrid>
so if i had in the ViewModel:
void delete(Line l)
{
Lines.Remove(l); //if you delete everything (grid empty) there won't be any error shown.
}
The error border & icon wouldn't show up around the datagrid.
But if instead i put a event that directly changed the ItemsSource like this:
void delete(Line l)
{
Lines.Remove(l);
myView.mygrid.ItemsSource = Lines; // this magically fixes everything... even though it was already bound to Lines... though i hate to directly access the View from within the ViewModel.
}
I'm not sure why exactly... but that fixed it.
Any ideas on how i could separate the view from the VM? I don't really like this fix.

Try to put the datagrid in a border and attach a trigger to it to make it red once datagrid is empty
<Border Margin="20" >
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ISEmpty}" Value="True">
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<DataGrid Name="myGrid" AutoGenerateColumns="True" ItemsSource="{Binding Path=Lecturers}" >
</DataGrid>
</Border>
Set IsEmpty property to true when you clear the grid
private bool iSEmpty;
public bool ISEmpty
{
get { return iSEmpty; }
set
{
iSEmpty = value;
NotifyPropertyChanged("ISEmpty");
}
}
Clear your item source collection
viewmodel.Lecturers.Clear();
this.viewmodel.ISEmpty = true;
Future work. You can bind trigger to datagrid control.

Related

Getting the boolean value of a CheckBox cell in WPF

I have a DataGrid that has a Checkbox in every first cell of a row. I want to loop through my DataGrid rows and get the rows if the Checkbox in that cell is checked.
This is my code in checking the DataGrid:
Edited based on Ilan's answer:
Private Sub ApproveBtn_Clicked(sender As Object, e As RoutedEventArgs)
For i As Integer = 0 To TimeSheetAppDGrid.Items.Count - 1
TimeSheetAppDGrid.SelectedItem = TimeSheetAppDGrid.Items(i)
Dim row As Row = DirectCast(TimeSheetAppDGrid.SelectedItems(0), Row)
Dim mycheckbox = TryCast(TimeSheetAppDGrid.Columns(0).GetCellContent(TimeSheetAppDGrid.Items(i)), ContentPresenter)
If mycheckbox Is Nothing Then
Return
End If
Dim checkBox = GetAllVisualChildren(mycheckbox).OfType(Of CheckBox)().FirstOrDefault(Function(box) box.Name = "chkSelectAll")
If checkBox Is Nothing Then
Return
End If
Dim isChecked = checkBox.Checked
If isChecked = True Then
MsgBox(row.Mark)
End If
Next
End Sub
The code below doesn't seem to be accepted by VB.NET causing the compilation error that says "Value of type 'FrameworkElement' cannot be converted to 'CheckBox'." After searching the web, almost all of the answers have this code in it but in C# so I converted it:
Dim mycheckbox As CheckBox = TryCast(TimeSheetAppDGrid.Columns(0).GetCellContent(TimeSheetAppDGrid.SelectedItems(0)), CheckBox)
This link is where I got the code. Below is the original line of code from C#:
CheckBox mycheckbox = myGrid.Columns[5].GetCellContent(myGrid.Items[i]) as CheckBox;
This is my xaml for the DataGrid:
<DataGrid Name ="TimeSheetAppDGrid" Margin="16,200,48,0" CanUserSortColumns="False" CanUserReorderColumns="False" CanUserAddRows="False" AutoGenerateColumns="False" SelectionUnit="FullRow"
materialDesign:DataGridAssist.CellPadding="13 8 8 8" materialDesign:DataGridAssist.ColumnHeaderPadding="8" IsReadOnly="True" SelectionMode="Single" >
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource MaterialDesignDataGridRow}">
<EventSetter Event="MouseDoubleClick" Handler="Row_DoubleClick"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<CheckBox Name="AllChkBox" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Name="chkSelectAll" Margin="3.5 0 0 0"
IsChecked="{Binding IsChecked, ElementName=AllChkBox,
Mode=OneWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding Path=Mark}" Header="Mark" Width="120" MaxWidth="120"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=ProjectCode}" Header="Project Code" Width="130" MaxWidth="130"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=Project}" Header="Project" Width="250" MaxWidth="250"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=PM}" Header="Project Manager" Width="230" MaxWidth="230"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=Activity}" Header="Activity" Width="600" MaxWidth="600"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=Today}" Header="Date" Width="200" MaxWidth="200"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=Regular}" Header="Regular" Width="130" MaxWidth="130"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"/>
<materialDesign:MaterialDataGridTextColumn Binding="{Binding Path=Overtime}" Header="Overtime" Width="130" MaxWidth="130"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}" BasedOn="{StaticResource MaterialDesignDataGridColumnHeader}">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</DataGridTextColumn.ElementStyle>
</materialDesign:MaterialDataGridTextColumn>
</DataGrid.Columns>
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}" BasedOn="{StaticResource MaterialDesignDataGridCell}">
<Setter Property="BorderThickness" Value="0"/>
</Style>
</DataGrid.CellStyle>
</DataGrid>
Any suggestions?
You have to take look at the visual tree generated using Snoop, so that your cast works properly.
Simplest approach is to keep updating a Dictionary<int, DataGridRow> upon Checked/Unchecked events of the CheckBox.
Sorry the code is in C#.
XAML :
<CheckBox Name="AllChkBox" Checked="AllChkBox_Checked" Unchecked="AllChkBox_Unchecked" />
<CheckBox Name="chkSelectAll" Margin="3.5 0 0 0"
Checked="chkSelectAll_Checked" Unchecked="chkSelectAll_Unchecked"
IsChecked="{Binding IsChecked, ElementName=AllChkBox, Mode=OneWay}"/>
Code :
Dictionary<int, DataGridRow> rows = new Dictionary<int, DataGridRow>();
private void chkSelectAll_Checked(object sender, RoutedEventArgs e)
{
var cur_item = DGrid.CurrentItem;
int cur_index = DGrid.Items.IndexOf(cur_item);
if (cur_index == -1) // header checkbox is checked
return;
DataGridRow row = (DataGridRow)DGrid.ItemContainerGenerator.ContainerFromItem(cur_item);
rows.Add(DGrid.Items.IndexOf(cur_item), row);
}
private void chkSelectAll_Unchecked(object sender, RoutedEventArgs e)
{
var cur_item = DGrid.CurrentItem;
rows.Remove(DGrid.Items.IndexOf(cur_item));
}
private void AllChkBox_Checked(object sender, RoutedEventArgs e)
{
foreach (var item in DGrid.Items)
{
DataGridRow row = (DataGridRow)DGrid.ItemContainerGenerator.ContainerFromItem(item);
rows.Add(DGrid.Items.IndexOf(item), row);
}
}
private void AllChkBox_Unchecked(object sender, RoutedEventArgs e)
{
rows.Clear();
}
EDIT
ok, here's a more refined way of achieving what you need without having to search the visual tree looking for the checkbox (again in c# but hopefully shouldn't be difficult to translate).
Firstly, i've created a Student class to bind to which implements INotifyPropertyChanged :
public class Student:INotifyPropertyChanged
{
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set { _isChecked = value;
OnPropertyChanged("IsChecked");
}
}
public string Mark { get; set; }
public string ProjectCode { get; set; }
public string Project { get; set; }
public string PM { get; set; }
public string Activity { get; set; }
public DateTime Today { get; set; }
public string Regular { get; set; }
public string Overtime { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I then add a row to my data grid with test data :
TimeSheetAppDGrid.ItemsSource = new List<Student>()
{
new Student { ProjectCode="Project1",Mark = "A",Today = DateTime.Today}
};
In the XAML for the data template i've changed the binding to not bind to the header check box but bind to the student IsChecked property and i've added a clicked handler for AllChkBox checkbox in the header cell:
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<CheckBox Click="AllChkBox_OnClick" Name="AllChkBox" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Name="chkSelectAll" Margin="3.5 0 0 0"
IsChecked="{Binding Path=IsChecked,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
For the AllChkBox clicked handler (AllChkBox_OnClick) i iterate through each data source (i.e. Student) and toggle the IsChecked property to the AllChkBox checked value:
private void AllChkBox_OnClick(object sender, RoutedEventArgs e)
{
foreach (var s in TimeSheetAppDGrid.ItemsSource)
{
Student st = s as Student;
st.IsChecked = (bool) AllChkBox.IsChecked;
}
}
Now each item source (i.e. Student) can be interrogated for their IsChecked property (which is bound to the corresponding checkbox cell) without the need to fish around checking for the checkbox (excuse the pun) i.e.
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
foreach (var source in TimeSheetAppDGrid.ItemsSource)
{
Student st = source as Student;
if (st.IsChecked)
MessageBox.Show("Is Checked");
}
}
in my opinion there is some conceptual error in your adopted solution:
CheckBox mycheckbox = myGrid.Columns[5].GetCellContent(myGrid.Items[i]) as CheckBox;
I've checked it and when we use the DataGridTemplateColumn.CellTemplate, like in your case, the value that is accepted(mycheckbox) is the ContentPresenter because there is the DataTemplate behind the cell(because you are using the DataTemplate). That is why the code is actually isn't working.
The solution that I'm suggesting you is to get the check box object from the ContentPresenter(the code is in C# you will have to convert it to VB), since it is the ContentPresenter's visual child(you can se this in Snoop).
UpdateHere is the GetAllVisualChildren code:
public static class VisualTreeHelperExtensions
{
public static T FindParent<T>(this DependencyObject child) where T : DependencyObject
{
while (true)
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
child = parentObject;
}
}
public static List<DependencyObject> GetAllVisualChildren(this DependencyObject parent)
{
var resultedList = new List<DependencyObject>();
var visualQueue = new Queue<DependencyObject>();
visualQueue.Enqueue(parent);
do
{
var depObj = visualQueue.Dequeue();
var childrenCount = VisualTreeHelper.GetChildrenCount(depObj);
for (int i = 0; i < childrenCount; i++)
{
var v = VisualTreeHelper.GetChild(depObj, i);
visualQueue.Enqueue(v);
}
resultedList.Add(depObj);
} while (visualQueue.Count > 0);
resultedList.RemoveAt(0);
return resultedList;
}
}
Do next things in order to import the VisualTreeHelperExtensions class to VB
Create a C# class library as the part of your solution.
Add the VisualTreeHelperExtensions class into a new created project.
Compile your C# class library.
Add the new create(and compiled) class library into your vb.net project as a reference(dll).
Update #3 - changed vb code
Dim mycheckbox = TryCast(TimeSheetAppDGrid.Columns(0).GetCellContent(TimeSheetAppDGrid.Items(0)), ContentPresenter)
If mycheckbox Is Nothing Then
Return
End If
Dim checkBox = mycheckbox.GetAllVisualChildren().OfType(Of CheckBox)().FirstOrDefault(Function(box) box.Name = "chkSelectAll")
If checkBox Is Nothing Then
Return
End If
Dim isChecked = checkBox.IsChecked
If isChecked = True Then
MsgBox("Checked")
End If
VB.net CheckBox example
CheckBox Control WPF In VB.NET.
CheckBox Class in .Net Framework(no any restriction to vb)
Since all that the .net CheckBox has its IsChecked boolean state presenter.
Regards.

WPF DataGrid CanUserAddRows = True

I seem to have a problem with adding rows to a DataGridthrough the interface itself.
Here is a screenshot of the UI:
As you can see, 0 rows were found in the database so nothing shows up in the DataGrid on the right side.
But id like there to be one empty row there, for manually adding rows.
The DataGrid.CanUserAddRows is set to True but has no effect.
Here is the xaml for the DataGrid, I have taken the liberty of removing some of the code to make it smaller.
PrivilegeDetailsView.xaml
<UserControl ...
d:DataContext="{d:DesignInstance impl:PrivilegeDetailsViewModel}">
<DataGrid ...
ItemsSource="{Binding RolesHasPrivilegesOnObjects}"
AutoGenerateColumns="False"
CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Type" CanUserSort="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type int:IRoleHasPrivilegeOnObjectListItemViewModel}">
<Image Source="{Binding Icon}" ToolTip="{Binding ToolTip}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" Header="Name" Binding="{Binding Name}"/>
<DataGridCheckBoxColumn Header="Select" Binding="{Binding HasSelect, UpdateSourceTrigger=PropertyChanged}">
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="CheckBox">
<Style.Triggers>
<DataTrigger Binding="{Binding CanHaveSelect}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</DataTrigger>
<DataTrigger Binding="{Binding CanHaveSelect}" Value="False">
<Setter Property="IsEnabled" Value="False"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridCheckBoxColumn.ElementStyle>
</DataGridCheckBoxColumn>
...
</DataGrid.Columns>
</DataGrid>
</UserControl>
PrivilegeDetailsView.xaml.cs
public partial class PrivilegeDetailsView : IPrivilegeDetailsView
{
public PrivilegeDetailsView() { InitializeComponent(); }
public DataGrid PrivilegesOnObjectsDataGrid { get { return PrivilegeDataGrid; } }
public IViewModel ViewModel
{
get { return (IViewModel)DataContext; }
set { DataContext = value; }
}
}
Here is the ViewModel (VM) for the xaml View above:
PrivilegeDetailsViewModel.cs
public class PrivilegeDetailsViewModel : ViewModelBase, IPrivilegeDetailsViewModel
{
private readonly IEventAggregator _eventAggregator;
private readonly IPrivilegeViewModel _privilegeViewModel;
private readonly IRoleHasPrivilegeOnObjectViewModelAdapterRepository _roleHasPrivilegeOnObjectViewModelAdapterRepository;
private ObservableCollection<IRoleHasPrivilegeOnObjectListItemViewModel> _rolesHasPrivilegesOnObjects;
public PrivilegeDetailsViewModel(IPrivilegeDetailsView view,
IRoleHasPrivilegeOnObjectViewModelAdapterRepository roleHasPrivilegeOnObjectViewModelAdapterRepository,
IPrivilegeViewModel privilegeViewModel,
IEventAggregator eventAggregator) : base(view)
{
_roleHasPrivilegeOnObjectViewModelAdapterRepository = roleHasPrivilegeOnObjectViewModelAdapterRepository;
_privilegeViewModel = privilegeViewModel;
_eventAggregator = eventAggregator;
Initialize();
}
protected override sealed void Initialize()
{
_privilegeViewModel.PropertyChanged += PrivilegeViewModelOnPropertyChanged;
_eventAggregator.GetEvent<ToggleSelectPrivilegeEvent>().Subscribe(ToggleSelectPrivilege);
...
}
public new IPrivilegeDetailsView View
{
get { return (IPrivilegeDetailsView)base.View; }
}
public ObservableCollection<IRoleHasPrivilegeOnObjectListItemViewModel> RolesHasPrivilegesOnObjects
{
get { return _rolesHasPrivilegesOnObjects; }
set
{
_rolesHasPrivilegesOnObjects = value;
OnPropertyChanged();
}
}
public void Save()
{
if(RolesHasPrivilegesOnObjects == null) return;
_roleHasPrivilegeOnObjectViewModelAdapterRepository.SaveChanges(RolesHasPrivilegesOnObjects);
}
private void ToggleExecutePrivilege(object obj)
{
var toggle = !View.PrivilegesOnObjectsDataGrid.SelectedItems.Cast<IRoleHasPrivilegeOnObjectListItemViewModel>()
.All(x => x.HasExecute);
foreach(var selectedItem in View.PrivilegesOnObjectsDataGrid
.SelectedItems
.Cast<IRoleHasPrivilegeOnObjectListItemViewModel>()
.Where(selectedItem => selectedItem.Object
.CanHavePrivilege("EXECUTE"))) {
selectedItem.HasExecute = toggle;
}
}
...
private void PrivilegeViewModelOnPropertyChanged(object s, PropertyChangedEventArgs e)
{
switch(e.PropertyName)
{
//When the SelectedSchema changes in the parent VM, I get the new rows to be shown in the DataGrid.
case "SelectedSchema":
RolesHasPrivilegesOnObjects = _roleHasPrivilegeOnObjectViewModelAdapterRepository
.GetPrivilegesOnObjectsAssociatedWith((IRoleEntityViewModel)_privilegeViewModel.SelectedRole,
(IContainerEntityViewModel)_privilegeViewModel.SelectedSchema);
break;
}
}
}
This is the VM for each row in the DataGrid
RoleHasPrivilegeOnObjectEntityViewModel.cs
public class RoleHasPrivilegeOnObjectEntityViewModel : EntityViewModelBase<RoleHasPrivilegeOnObjectEntityViewModel,
RoleHasPrivilegesOnObject>,
IRoleHasPrivilegeOnObjectListItemViewModel
{
private readonly RoleHasPrivilegesOnObject _roleHasPrivilegesOnObject;
public RoleHasPrivilegeOnObjectEntityViewModel(RoleHasPrivilegesOnObject roleHasPrivilegesOnObject)
{
_roleHasPrivilegesOnObject = roleHasPrivilegesOnObject;
Role = new RoleEntityViewModel(_roleHasPrivilegesOnObject.Role);
Object = new ObjectEntityViewModel(_roleHasPrivilegesOnObject.Object);
}
public override EntityType EntityType { get { return EntityType.NONE; } }
public override RoleHasPrivilegesOnObject OriginalEntity { get { return _roleHasPrivilegesOnObject; } }
public IRoleEntityViewModel Role { get; set; }
public IObjectEntityViewModel Object { get; set; }
public string ToolTip { get { return _roleHasPrivilegesOnObject.ToolTip; } }
public bool HasExecute
{
get { return _roleHasPrivilegesOnObject.HasExecute; }
set
{
_roleHasPrivilegesOnObject.HasExecute = value;
OnPropertyChanged();
}
}
public bool CanHaveExecute { get { return _roleHasPrivilegesOnObject.CanHaveExecute; } }
public override string Icon { get { return Object != null ? Object.Icon : string.Empty; } }
public override string NAME
{
get { return _roleHasPrivilegesOnObject.NAME; }
set
{
_roleHasPrivilegesOnObject.NAME = value;
OnPropertyChanged();
}
}
...
}
I know this is a lot of code, I have stripped away a lot and put in place a few dots (...) to show that more code exist. NOTE: Im using EF5 and PRISM
How can I make the DataGrid accept new rows through the GUI?
I believe your problem is using ObservableCollection<IRoleHasPrivilegeOnObjectListItemViewModel> as ItemsSource. In order for DataGrid to be able to create a new row, there has to be a type that can be constructed with an empty constructor.
If you changed it to say ObservableCollection<RoleHasPrivilegeOnObjectEntityViewModel> instead, i'm pretty sure your rows will start getting added.
What I ended up doing was partially/Mostly what Maverik suggested.
I changed ObservableCollection<IRoleHasPrivilegeOnObjectListItemViewModel> to be ObservableCollection<RoleHasPrivilegeOnObjectEntityViewModel> and created a default constructor, which it didn't previously have.
The issue then was that the RoleHasPrivilegeOnObjectEntityViewModel needs some fields and properties set in order to function, so I created a public Initialize function to provided the necessary parameters.
I added an event handler to the DataGrid's InitializingNewItem event, where i called the Initialize function.
private void PrivilegesOnObjectsDataGridOnInitializingNewItem(object s, InitializingNewItemEventArgs e)
{
var newItem = e.NewItem as RoleHasPrivilegeOnObjectEntityViewModel;
if (newItem == null) return;
var role = _privilegeViewModel.SelectedRole;
var schema = _privilegeViewModel.SelectedSchema;
newItem.Initialize(role.OriginalEntity, schema.OriginalEntity);
}
When trying to adda new row, clicking the ComboBox didn't fire off the InitializeNewItem event. But clicking any other column fired off the InitializeNewItem event, and since at first each Row's VM had it's own AvailableObjectTypes property, the ComboBox ItemSource was not set if the ComboBox was selected before any other column, thus making it empty.
That was not an acceptable behaviour so moving AvailableObjectTypes to the PrivilegeDetailsViewModel and changing the ComboBox's ItemSource binding to this helped
ItemsSource="{Binding DataContext.AvailableObjectTypes, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"

WPF Datagrid SelectedItem loses binding after edit

EDITED: As comments have suggested, I should implement the MVVM pattern, I have done exactly that. However the same problem still persists. So I have altered the question accordingly:
I have a datagrid containing two columns bound to an observable collection (in the MyNotes class). One column contains a combobox and the other a textbox. The collection stores references to Note objects that contain an enumeration variable (displayed by the combobox) and a string (displayed by the textbox). All works fine except for the SelectedItems (and therefore the SelectedItem). When the program is built and run, you can add new rows to the datagrid (using the add/remove buttons) but after you attempt an edit (by entering the datagrid's textbox or combobox) then the datagrid's selectedItems and selectedItem fail. This can be seen by the use of the add/remove buttons: the selected row is not deleted and a new row is not added above the selected row respectively. This is a result of a symptom regarding the SelectedNote property losing its binding (I don't know why this happens and when I attempt to hack a rebind, the rebind fails?). Another symptom relates to the selected items property not reflecting what the datagrid is actually showing as selected (when viewing it in debug mode).
I am sure this problem relates to an issue with the datagrid, which makes it unusable for my case.
Here is the new XAML (its datacontext, the viewmodel, is set in the XAML and ParaTypes and headerText are both XAML static resources):
<DataGrid x:Name ="dgdNoteLimits"
ItemsSource ="{Binding ParagraphCollection}"
SelectedItem ="{Binding Path=SelectedNote, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AllowDrop ="True"
HeadersVisibility ="Column"
AutoGenerateColumns ="False"
CanUserAddRows ="False"
CanUserReorderColumns ="False"
CanUserSortColumns ="False"
BorderThickness ="0"
VerticalGridLinesBrush ="DarkGray"
HorizontalGridLinesBrush="DarkGray"
SelectionMode ="Extended"
SelectionUnit ="FullRow"
ColumnHeaderStyle ="{StaticResource headerText}">
<DataGrid.ItemContainerStyle>
<Style>
<Style.Resources>
<!-- SelectedItem's background color when focused -->
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="Blue"/>
<!-- SelectedItem's background color when NOT focused -->
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}"
Color="Blue" />
</Style.Resources>
</Style>
</DataGrid.ItemContainerStyle>
<DataGrid.Columns>
<DataGridComboBoxColumn Header = "Note Type"
ItemsSource = "{Binding Source={StaticResource ParaTypes}}"
SelectedValueBinding= "{Binding Path=NoteType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextBinding = "{Binding Path=NoteType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
MinWidth = "115"
Width = "Auto">
</DataGridComboBoxColumn>
<DataGridTextColumn Header ="Description"
Binding="{Binding Path=NoteText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width ="*">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="TextWrapping"
Value ="Wrap"/>
</Style>
</DataGridTextColumn.ElementStyle>
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="TextBox">
<Setter Property="SpellCheck.IsEnabled"
Value ="true" />
<Setter Property="TextWrapping"
Value ="Wrap"/>
</Style>
</DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="1"
Margin="0, 0, 0, 16"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Add"
Width="72"
Margin="16,8,8,8"
Command="{Binding AddClickCommand}"/>
<Button Content="Remove"
Width="72"
Margin="16,8,8,8"
Command="{Binding RemoveClickCommand}"/>
</StackPanel>
Here is the view model:
class MainWindowViewModel : INotifyPropertyChanged
{
MyNotes NotesCollection;
private bool canExecute;
private ICommand clickCommand;
public MainWindowViewModel()
{
this.NotesCollection = new MyNotes();
this.ParagraphCollection = this.NotesCollection.Notes;
this.canExecute = true;
}
private ObservableCollection<Note> paragraphCollection;
public ObservableCollection<Note> ParagraphCollection
{
get { return this.paragraphCollection; }
set
{
this.paragraphCollection = value;
RaisePropertyChanged(() => this.ParagraphCollection);
}
}
private Note selectedNote;
public Note SelectedNote
{
get { return this.selectedNote; }
set
{
if (this.selectedNote == value)
return;
this.selectedNote = value;
RaisePropertyChanged(() => this.SelectedNote);
}
}
public ICommand AddClickCommand
{
get
{
return this.clickCommand ?? (new ClickCommand(() => AddButtonHandler(), canExecute));
}
}
public void AddButtonHandler()
{
int noteIndex = 0;
Note aNote;
// what to do if a note is either selected or unselected...
if (this.SelectedNote != null)
{
// if a row is selected then add row above it.
if (this.SelectedNote.NoteIndex != null)
noteIndex = (int)this.SelectedNote.NoteIndex;
else
noteIndex = 0;
//create note and insert it into collection.
aNote = new Note(noteIndex);
ParagraphCollection.Insert(noteIndex, aNote);
// Note index gives sequential order of collection
// (this allows two row entries to have same NoteType
// and NoteText values but still note equate).
int counter = noteIndex;
// reset collection index so they are sequential
for (int i = noteIndex; i < this.NotesCollection.Notes.Count; i++)
{
this.NotesCollection.Notes[i].NoteIndex = counter++;
}
}
else
{
//if a row is not selected add it to the bottom.
aNote = new Note(this.NotesCollection.Count);
this.ParagraphCollection.Add(aNote);
}
}
public ICommand RemoveClickCommand
{
get
{
return this.clickCommand ?? (new ClickCommand(() => RemoveButtonHandler(), canExecute));
}
}
public void RemoveButtonHandler()
{
//delete selected note.
this.ParagraphCollection.Remove(selectedNote);
}
//boiler plate INotifyPropertyChanged implementation!
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged<T>(Expression<System.Func<T>> propertyExpression)
{
var memberExpr = propertyExpression.Body as MemberExpression;
if (memberExpr == null)
throw new ArgumentException("propertyExpression should represent access to a member");
string memberName = memberExpr.Member.Name;
RaisePropertyChanged(memberName);
}
protected virtual void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
and the model:
class MyNotes
{
public ObservableCollection<Note> Notes;
public MyNotes()
{
this.Notes = new ObservableCollection<Note>();
}
}
public enum NoteTypes
{
Header, Limitation, Warning, Caution, Note
}
public class Note
{
public int? NoteIndex { get; set; }
public NoteTypes NoteType { get; set; }
public string NoteText { get; set; }
public Note()
{
this.NoteIndex = null;
this.NoteType = NoteTypes.Note;
this.NoteText = "";
}
public Note(int? noteIndex): this()
{
this.NoteIndex = noteIndex;
}
public override string ToString()
{
return this.NoteType + ": " + this.NoteText;
}
public override bool Equals(object obj)
{
Note other = obj as Note;
if (other == null)
return false;
if (this.NoteIndex != other.NoteIndex)
return false;
if (this.NoteType != other.NoteType)
return false;
if (this.NoteText != other.NoteText)
return false;
return true;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + this.NoteIndex.GetHashCode();
hash = hash * 23 + this.NoteType.GetHashCode();
hash = hash * 23 + this.NoteText.GetHashCode();
return hash;
}
}
Existing comments were greatly appreciated (I have learnt a lot and see the value of MVVM). It is just a shame they have not resolved the problem. But thank you.
So if anyone knows how I can resolve this issue, then that would be greatly appreciated.

WPF Autogenerated DataGrid Cell changed event when bound to ItemSource

I have a simple datagrid which has columns autogenerated and is bound to an item source. This item source is updated at some intervals and I can't find how to fire an event for a single cell changed. I want to change the color of the cell based on if the update to the data source changed the previous value of the cell.
I looked at Highlighting cells in WPF DataGrid when the bound value changes as well as http://codefornothing.wordpress.com/2009/01/25/the-wpf-datagrid-and-me/ but I am still unsure about how to go about implementing this. Some example code would be really helpful to get started on the right path.
If you're binding to a DataTable, I don't think this would be a productive path to go down. Doing any kind of styling based on the contents of a DataTable bound DataGrid is nearly impossible in WPF. There are several suggestions on StackOverflow, but they are usually pretty hacky, event-driven (which is generally bad news in WPF), and a maintenance nightmare.
If however, the ItemsSource you are binding to is an ObservableCollection, where RowViewModel is the class that represents the data in a single row of the DataGrid, then it shouldn't be too bad. Make sure that RowViewModel implements INotifyPropertyChanged, and simply update the individual RowViewModels with their updated data. Then, you can add the logic to expose an additional property on your RowViewModel that indicates if a particular value is new - just use some styles/triggers in the XAML to set the background color based on the value of this new property.
Here's an example of the latter: If you edit one of values in the first column, it will turn the cell red. The same thing will happen if you change the value in your ItemViewModel programmatically via Database update.
The XAML:
<Window x:Class="ShowGridUpdates.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">
<Grid>
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Item1, UpdateSourceTrigger=PropertyChanged}">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Style.Setters>
<Setter Property="Background" Value="Blue"/>
<Setter Property="Foreground" Value="White"/>
</Style.Setters>
<Style.Triggers>
<DataTrigger Binding="{Binding Item1Changed}" Value="True">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Item2}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
The Code-Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel : PropertyChangedNotifier
{
public ViewModel()
{
Items = new ObservableCollection<ItemViewModel>()
{
new ItemViewModel(){Item1="Item1FistValue", Item2="Item2FirstValue"},
new ItemViewModel(){Item1="whocareswhatvalue", Item2="Icertainlydont"}
};
//just to get the initial state correct
foreach (var item in Items)
{
item.Item1Changed = false;
}
}
private ObservableCollection<ItemViewModel> _items;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
set
{
_items = value;
OnPropertyChanged("Items");
}
}
}
public class ItemViewModel : PropertyChangedNotifier
{
private string _item1;
private string _item2;
private bool _item1Changed;
public bool Item1Changed
{
get
{
return _item1Changed;
}
set
{
_item1Changed = value;
OnPropertyChanged("Item1Changed");
}
}
public string Item1
{
get
{
return _item1;
}
set
{
if (_item1 != value)
Item1Changed = true;
else
Item1Changed = false;
_item1 = value;
OnPropertyChanged("Item1");
}
}
public string Item2
{
get
{
return _item2;
}
set
{
_item2 = value;
OnPropertyChanged("Item2");
}
}
}
public class PropertyChangedNotifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Force validation on bound controls in WPF

I have a WPF dialog with a couple of textboxes on it.
Textboxes are bound to my business object and have WPF validation rules attached.
The problem is that user can perfectly click 'OK' button and close the dialog, without actually entering the data into textboxes. Validation rules never fire, since user didn't even attempt entering the information into textboxes.
Is it possible to force validation checks and determine if some validation rules are broken?
I would be able to do it when user tries to close the dialog and prohibit him from doing it if any validation rules are broken.
Thank you.
In 3.5SP1 / 3.0SP2, they also added a new property to the ValidationRule base, namely, ValidatesOnTargetUpdated="True". This will call the validation as soon as the source object is bound, rather than only when the target control is updated. That may not be exactly what you want, but it's not bad to see initially all the stuff you need to fix.
Works something like this:
<TextBox.Text>
<Binding Path="Amount" StringFormat="C">
<Binding.ValidationRules>
<validation:RequiredValidationRule
ErrorMessage="The pledge amount is required."
ValidatesOnTargetUpdated="True" />
<validation:IsNumericValidationRule
ErrorMessage="The pledge amount must be numeric."
ValidationStep="ConvertedProposedValue"
ValidatesOnTargetUpdated="True" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
We have this issue in our application as well. The validation only fires when bindings update, so you have to update them by hand. We do this in the Window's Loaded event:
public void Window_Loaded(object sender, RoutedEventArgs e)
{
// we manually fire the bindings so we get the validation initially
txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();
txtCode.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
This will make the error template (red outline) appear, and set the Validation.HasError property, which we have triggering the OK button to disable:
<Button x:Name="btnOK" Content="OK" IsDefault="True" Click="btnOK_Click">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="false" />
<Style.Triggers>
<!-- Require the controls to be valid in order to press OK -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=txtName, Path=(Validation.HasError)}" Value="false" />
<Condition Binding="{Binding ElementName=txtCode, Path=(Validation.HasError)}" Value="false" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="true" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Here is an alternative way that doesn't require calling "UpdateSource()" or "UpdateTarget()":
var binding = thingToValidate.GetBinding(propertyToValidate);
foreach (var rule in binding.ValidationRules)
{
var value = thingToValidate.GetValue(propertyToValidate);
var result = rule.Validate(value, CultureInfo.CurrentCulture);
if (result.IsValid)
continue;
var expr = BindingOperations.GetBindingExpression(thingToValidate, propertyToValidate);
if (expr == null)
continue;
var validationError = new ValidationError(rule, expr);
validationError.ErrorContent = result.ErrorContent;
Validation.MarkInvalid(expr, validationError);
}
Just in case anyone happens to find this old question and is looking for an answer that addresses Monstieur's comment about UI guidelines, I did the following:
Xaml
<TextBox.Text>
<Binding Path="TextValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:RequiredFieldValidationRule>
<local:RequiredFieldValidationRule.IsRequiredField>
<local:BoolValue Value="{Binding Data.Required, Source={StaticResource proxy}}" />
</local:RequiredFieldValidationRule.IsRequiredField>
<local:RequiredFieldValidationRule.ValidationFailed>
<local:BoolValue Value="{Binding Data.HasValidationError, Mode=TwoWay, Source={StaticResource proxy}}" />
</local:RequiredFieldValidationRule.ValidationFailed>
</local:RequiredFieldValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
RequiredFieldValidationRule:
public class RequiredFieldValidationRule : ValidationRule
{
private BoolValue _isRequiredField;
public BoolValue IsRequiredField
{
get { return _isRequiredField; }
set { _isRequiredField = value; }
}
private BoolValue _validationFailed;
public BoolValue ValidationFailed
{
get { return _validationFailed; }
set { _validationFailed = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
ValidationFailed.Value = IsRequiredField.Value && (value == null || value.ToString().Length == 0);
return new ValidationResult(!ValidationFailed.Value, ValidationFailed.Value ? "This field is mandatory" : null);
}
}
In the class that the Xaml binds to
private bool _hasValidationError;
public bool HasValidationError
{
get { return _hasValidationError; }
set { _hasValidationError = value; NotifyPropertyChanged(nameof(HasValidationError)); }
}
public void InitialisationMethod() // Or could be done in a constructor
{
_hasValidationError = Required; // Required is a property indicating whether the field is mandatory or not
}
I then hide my Save button using a bound property, if any of my objects has HasValidationError = true.
Hope this is helpful to someone.
Use the method above proposed by Robert Macnee. For example:
//force initial validation
foreach (FrameworkElement item in grid1.Children)
{
if (item is TextBox)
{
TextBox txt = item as TextBox;
txt.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
But, BE SURE that the bound controls are Visibles before this code run!
using the INotifyPropertychanged on your data object
public class MyObject : INotifyPropertyChanged
{
string _MyPropertyToBind = string.Empty;
public string MyPropertyToBind
{
get
{
return _MyPropertyToBind;
}
set
{
_MyPropertyToBind = value;
NotifyPropertyChanged("MyPropertyToBind");
}
}
public void NotifyPropertyChanged(string property)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
you can add the following code to your control
<TextBox Text="{Binding MyPropertyToBind, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
The textbox susbscribe to the propertychanged event of the datacontext object ( MyObjet in our example) and assumes it is fired when the source data has been updated
it automatically forces the refresh to the control
No need to call yourself the UpdateTarget method

Resources