Change DataGrid column visibility (WPF, MVVM) - c#

I just recently started learning MVVM. I hope that a solution to this problem will come.
In my application, the user is authorized in the system, after which a window with a table opens. Users are divided into roles: Administrator and Employee. I want the Employee to be unable to see a certain column (ID).
I have an AuthorizationMeth class, where the IDRoleAuthorization variable stores role ID of the authorized user. How can I now use this value to hide the column ID? In my case if IDRoleAuthorization = 2 to hide column ID
Found solutions using the Freezable class and creating a FrameworkElement in XAML but I can't figure out how this is solved for my problem.
Methods/AuthorizationMeth.cs
public class AuthorizationMeth
{
public static int IDRoleAuthorization;
public bool Enter(string login, string password)
{
Intis6Context db = new Intis6Context();
if (login == "" || password == "")
{
MessageBox.Show("You have not completed all fields", "Authorization", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
var auth_check = db.Users.AsNoTracking().FirstOrDefault(ch => ch.Login == login && ch.Password == password);
if (auth_check == null)
{
MessageBox.Show("Login or password entered incorrectly", "Authorization", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
IDRoleAuthorization = auth_check.IdRole;
return true;
}
}
View/ContractView.xaml
<DataGrid Background="White" AutoGenerateColumns="False" EnableColumnVirtualization="True" EnableRowVirtualization="True"
ItemsSource="{Binding AllContrsupl_saleDTO, IsAsync=True}"
Grid.Row="0">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Path=Cnssid}"/>
<DataGridTextColumn Header="Author" Binding="{Binding Path=FULLNAMEstaff}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Path=typeTable}"/>

Have you tried something like this?
If you want to Hide the "ID" column try this:
You need to assign a x:Name to your DataGrid control first. For example "myDataGrid" Then you can do this in Code behind.
if(IDRoleAuthorization == 2)
{
myDataGrid.Columns[0].Visibility = Visibility.Collapsed;
}
Hope this helps.

Don't expose a public static int IDRoleAuthorization field. Such a field must be a property (never define public fields) and at least read-only, and a read-only instance property (non-static) at best. Additionally, don't expose the numeric value, but a bool property e.g., IsAuthorized. The logic to determine whether the numeric code evaluates to an authorized user must be encapsulated and not spread across the application. External classes must depend on the result of this evaluation only.
The DataGridColumn definitions are not part of the visual tree. They are not rendered. they are just placeholders that contain information about the column, which will be generated by the DataGrid later. The actual column consists of a DataGridColumnHeader and DataGridCell elements.
Because of this, you can't configure a Binding on the DataGridColumn.Visbility property.
You can now simply toggle the cells and their associated header (which will not remove the column, but the values of the header and cells):
<!-- ToggleButton to simulate the IsAuthorized property -->
<ToggleButton x:Name="ToggleButton" Content="Hide/show column content" />
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn Header="Dynamic Column">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ToggleButton, Path=IsChecked}" Value="True">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ToggleButton, Path=IsChecked}" Value="True">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.HeaderStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Static Column" />
</DataGrid.Columns>
</DataGrid>
To remove the column completely in your scenario (no auto-generated columns), you must remove it manually from the DataGrid.Columns collection or toggle the column definitions Visibilty explicitly (by accessing the DataGrid.Columns).
Add and AuthorizationChanged event to your view model class and make the view listen to it. In the event handler set the columns visibility or remove/add the column.
Alternatively, write a simple attached behavior:
DataGridHelper.cs
class DataGridHelper : DependencyObject
{
public static string GetHidableColumnIndices(DependencyObject attachingElement) => (string)attachingElement.GetValue(HidableColumnIndicesProperty);
public static void SetHidableColumnIndices(DependencyObject attachingElement, string value) => attachingElement.SetValue(HidableColumnIndicesProperty, value);
public static readonly DependencyProperty HidableColumnIndicesProperty = DependencyProperty.RegisterAttached(
"HidableColumnIndices",
typeof(string),
typeof(DataGridHelper),
new PropertyMetadata(default(string), OnHidableColumnIndicesChanged));
public static Visibility GetColumnVisibility(DependencyObject attachingElement) => (Visibility)attachingElement.GetValue(ColumnVisibilityProperty);
public static void SetColumnVisibility(DependencyObject attachingElement, Visibility value) => attachingElement.SetValue(ColumnVisibilityProperty, value);
public static readonly DependencyProperty ColumnVisibilityProperty = DependencyProperty.RegisterAttached(
"ColumnVisibility",
typeof(Visibility),
typeof(DataGridHelper),
new PropertyMetadata(default(Visibility), OnColumnVisibilityChanged));
private static void OnColumnVisibilityChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (attachingElement is not DataGrid dataGrid)
{
throw new ArgumentException("Attaching element must be of type DataGrid.");
}
ToggleColumnVisibility(dataGrid);
}
private static void OnHidableColumnIndicesChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (attachingElement is not DataGrid dataGrid)
{
throw new ArgumentException("Attaching element must be of type DataGrid.");
}
ToggleColumnVisibility(dataGrid);
}
private static void ToggleColumnVisibility(DataGrid dataGrid)
{
IEnumerable<int> columnIndices = GetHidableColumnIndices(dataGrid)
.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(numericChar => int.Parse(numericChar));
foreach (int columnIndex in columnIndices)
{
dataGrid.Columns[columnIndex].Visibility = GetColumnVisibility(dataGrid);
}
}
}
Usage example
<DataGrid DatGridHelper.HidableColumnIndices="0,3,8"
DataGridHelper.ColumnVisiblility="{Binding IsAuthenticated, Converter={StaticResource BooleanToVisibilityConverter}}" />

Related

Display deleted rows in a DataTable

So I have a DataTable that I bind to the DataGrid in XAML.
User is allowed to add, modify and remove rows for a table. I would like to mark rows with a specific colour, depending on the action that user makes. For instance, if user adds a row, that row will be marked as Green. If user modifies a row, that row will then be marked as orange. And if user removes the row, that row will be marked as red.
The problem that I have is that the removed row is no longer visible once I call row.Delete(); from a view model.
Is there a way to keep a DataRow marked for removal shown in a DataGrid? I know how to achieve the row background effect, depending on user action. The only problem is I don't know how to keep the deleted row visible. The idea is that user can either revert changes or apply them and that's when the pending deletion row should be actually deleted.
EDIT (Added example on how I update row background colour):
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Row.RowState}" Value="{x:Static data:DataRowState.Deleted}" />
<Condition Binding="{Binding RelativeSource={RelativeSource Mode=Self}, Path=IsSelected}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Background" Value="IndianRed" TargetName="DGR_Border"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontWeight" Value="Bold"/>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
I think, when user marks row for deleting - you should save it index somewhere (int[] array or List<int> for example), then call yourDataGridView.Rows.RemoveAt(index) for each element in that collection when user finished working with table.
Maybe something like:
//Collection for rows indexes which will be deleted later
List<int> rowsToDelete = new List<int>();
//Call this when user want to delete element
private void MarkElementsToRemove()
{
if (yourDataGrid.SelectedItems.Count > 0)
{
//Get selected by user rows
for (int i = 0; i < yourDataGrid.SelectedItems.Count; ++i)
{
DataGridRow row = (DataGridRow)yourDataGrid.SelectedItems[i];
//Fill rows background with some color to mark them visually as "deleted"
row.Background = new SolidColorBrush(Color.FromRgb(255, 0, 0));
//Get row index to remove it later and add it to collection
rowsToDelete.Add(row.GetIndex());
}
}
}
// Call this when user finished work with DataGrid and items may be removed
private void RemoveMarkedElements()
{
foreach (int index in rowsToDelete)
{
yourDataGrid.Items.RemoveAt(index);
}
rowsToDelete.Clear();
}
Instead of index you may save whole DataGridRow and call yourDataGrid.Remove(wholeRow);.
And for reverse deletion, you just unmark it by removing color and removing row index or whole row from a collection.
If I understood you correctly, you need to use the Delete key not to delete lines, but to put a marker on them.
And in the DataGrid, you need to highlight color the rows marked with this marker.
You have not shown your table, so I will demonstrate in my simple conciliation.
The example uses the BaseInpc and RelayCommand classes.
In addition to them, the command extension method is used:
using System.Windows.Input;
namespace Simplified
{
public static class CommandExtensionMethods
{
public static bool TryExecute(this ICommand command, object parameter)
{
bool can = command.CanExecute(parameter);
if (can)
command.Execute(parameter);
return can;
}
public static bool TryExecute(this ICommand command)
=> TryExecute(command, null);
}
}
ViewModel:
using Simplified;
using System.Data;
using System.Windows.Input;
namespace DeferredRowDeletion
{
public class DrdViewModel : BaseInpc
{
public DataTable Table { get; } = new DataTable();
public DrdViewModel()
{
Table.Columns.Add("Name", typeof(string));
Table.Columns.Add("Value", typeof(int));
Table.Columns.Add("Marked for deletion", typeof(bool));
foreach (var name in new string[] { "First", "Second", "Third", "Fourth", "Fifth" })
{
var row = Table.NewRow();
row[0] = name;
row[1] = Table.Rows.Count;
row[2] = Table.Rows.Count % 2 == 1;
Table.Rows.Add(row);
}
}
private ICommand _markRemoveChangeCommand;
private bool _isRemoveRowsImmediately;
public ICommand MarkRemoveChangeCommand => _markRemoveChangeCommand
?? (_markRemoveChangeCommand = new RelayCommand<DataRow>(
row => row[2] = !(bool)(row[2] ?? false),
row => !IsRemoveRowsImmediately
));
public bool IsRemoveRowsImmediately
{
get => _isRemoveRowsImmediately;
set => Set(ref _isRemoveRowsImmediately, value);
}
}
}
Window XAML:
<Window x:Class="DeferredRowDeletion.DrdWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DeferredRowDeletion"
mc:Ignorable="d"
Title="DrdWindow" Height="450" Width="800">
<FrameworkElement.DataContext>
<local:DrdViewModel/>
</FrameworkElement.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CheckBox Content="Removw Rows Immediately"
IsChecked="{Binding IsRemoveRowsImmediately}"
Margin="5"/>
<DataGrid x:Name="dataGrid" Grid.Row="1"
ItemsSource="{Binding Table, Mode=OneWay}"
AutoGeneratingColumn="OnAutoGeneratingColumn"
CanUserDeleteRows="{Binding IsRemoveRowsImmediately}"
PreviewKeyDown="OnPreviewKeyDown">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding [Marked for deletion]}" Value="true">
<Setter Property="Background" Value="HotPink"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</Window>
Code Behind Window:
using Simplified;
using System.Data;
using System.Windows;
using System.Windows.Input;
namespace DeferredRowDeletion
{
public partial class DrdWindow : Window
{
public DrdWindow()
{
InitializeComponent();
}
private void OnAutoGeneratingColumn(object sender, System.Windows.Controls.DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "Marked for deletion")
e.Cancel = true;
}
private void OnPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Delete)
{
DrdViewModel viewModel = (DrdViewModel)DataContext;
var rowView = dataGrid.CurrentItem as DataRowView;
if (rowView != null && !rowView.IsEdit)
viewModel.MarkRemoveChangeCommand.TryExecute(rowView.Row);
}
}
}
}
If you are unable to use this example, then write the reason and add details to the explanation of your question.
The answer is supplemented by clarifications for the added details:
I think I should've mentioned that I use DataRow's RowState property bound to the DataTrigger to update row background colour. Added details to the question.
To control the visibility of rows, you need to change the value of the DataTable.DefaultView.RowStateFilter property.
This is not hard to do in the ViewModel.
But an additional problem is that the RowState property does not notify about its change.
So the trigger binding won't work just like that.
In my example, I solved this by calling Items.Refresh ().
Perhaps you are using a different solution since you have not written about any problems associated with this.
using Simplified;
using System.Data;
using System.Windows.Input;
namespace DeferredRowDeletion
{
public class ShowDeletedRowsViewModel : BaseInpc
{
public DataTable Table { get; } = new DataTable();
public ShowDeletedRowsViewModel()
{
Table.Columns.Add("Name", typeof(string));
Table.Columns.Add("Value", typeof(int));
foreach (var name in new string[] { "First", "Second", "Third", "Fourth", "Fifth" })
{
var row = Table.NewRow();
row[0] = name;
row[1] = Table.Rows.Count;
Table.Rows.Add(row);
}
// Commits all the changes
Table.AcceptChanges();
Table.Rows[1].Delete();
Table.Rows[3].Delete();
// Show Deleded Rows
IsVisibilityDelededRows = true;
}
private ICommand _markRemoveChangeCommand;
private bool _isVisibilityDelededRows;
public ICommand MarkRemoveChangeCommand => _markRemoveChangeCommand
?? (_markRemoveChangeCommand = new RelayCommand<DataRow>(
row => IsVisibilityDelededRows ^= true,
row => !IsVisibilityDelededRows
));
public bool IsVisibilityDelededRows
{
get => _isVisibilityDelededRows;
set => Set(ref _isVisibilityDelededRows, value);
}
protected override void OnPropertyChanged(string propertyName, object oldValue, object newValue)
{
base.OnPropertyChanged(propertyName, oldValue, newValue);
if (propertyName == nameof(IsVisibilityDelededRows))
{
// Change the row filter if the associated property has changed
if (IsVisibilityDelededRows)
{
Table.DefaultView.RowStateFilter |= DataViewRowState.Deleted;
}
else
{
Table.DefaultView.RowStateFilter &= ~DataViewRowState.Deleted;
}
}
}
}
}
<Window x:Class="DeferredRowDeletion.SdrWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DeferredRowDeletion" xmlns:data="clr-namespace:System.Data;assembly=System.Data"
mc:Ignorable="d"
Title="SdrWindow" Height="450" Width="800">
<FrameworkElement.DataContext>
<local:ShowDeletedRowsViewModel/>
</FrameworkElement.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<CheckBox x:Name="cbAutoRefresh" Content="Auto Items.Refresh()" IsChecked="True" Margin="5"/>
<CheckBox Content="Visibility Deleded Rows"
IsChecked="{Binding IsVisibilityDelededRows}"
Margin="5"/>
</StackPanel>
<DataGrid x:Name="dataGrid" Grid.Row="1"
ItemsSource="{Binding Table, Mode=OneWay}"
PreviewKeyUp="OnPreviewKeyUp">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Row.RowState, Mode=OneWay}"
Header="RowState"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Row.RowState}" Value="{x:Static data:DataRowState.Deleted}">
<Setter Property="Background" Value="HotPink"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
</Window>
private void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete && cbAutoRefresh.IsChecked == true)
dataGrid.Items.Refresh();
}

ObservableCollection does not update => DataGrid UI does not show everything

I have a problem with some code i wrote.
I am trying to build a Applicant-Administration program.
I have a SQL-Server on which is my Database.
At the start of the program, a DataGrid is bound to a ObservableCollection
which fires a SQL SELECT via Dapper.
If I insert a new Applicant with another SQL query, the DataGrid does not show this new Applicant.
I debugged through my program to see whats going wrong.
I think it is the ObservableCollection on which I bind.
Code of DataGrid
<DataGrid x:Name="DgInformation" ItemsSource="{Binding AllData,Mode=TwoWay}"
SelectedItem="{Binding SelectedApplicantModel, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}"
AutoGenerateColumns="False"
Grid.Row="2"
MaxHeight="870"
IsSynchronizedWithCurrentItem="True"
IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
HorizontalAlignment="Center"
Margin="10,5,10,5"
Background="#F5F5F5"
AlternatingRowBackground="#eeeeee" AlternationCount="2">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<EventSetter Event="MouseDoubleClick" Handler="DG_Information_MouseDoubleClick" />
<EventSetter Event="KeyDown" Handler="DG_Information_KeyDown" />
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<materialDesign:MaterialDataGridTextColumn Header="Vorname" Width="110"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding FirstName,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Nachname" Width="110"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding LastName,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Geburtstag" Width="110"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Birthday,Mode=TwoWay, StringFormat=\{0:dd.MM.yy\}, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Schulabschluss" Width="140"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Graduation.Graduation,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="PLZ" Width="90"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding PostCode,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Ort"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Address.HomePlace, Mode =TwoWay,UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Geschlecht"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Gender.Gender,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<DataGridHyperlinkColumn Header="E-Mail"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding EMail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<DataGridHyperlinkColumn.ElementStyle>
<Style>
<EventSetter Event="Hyperlink.Click" Handler="DgHyperlinkClick" />
</Style>
</DataGridHyperlinkColumn.ElementStyle>
</DataGridHyperlinkColumn>
<materialDesign:MaterialDataGridTextColumn Header="Stelle"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Job,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<materialDesign:MaterialDataGridTextColumn Header="Bewertung"
EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
Binding="{Binding Grade.Grade, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
</DataGrid>
My ViewModel
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<ApplicantModel> _allData;
public BaseViewModel()
{
AllData = ApplicantService.GetInstance().GetAll();
}
public ObservableCollection<ApplicantModel> AllData
{
get { return _allData; }
set
{
_allData = value;
OnPropertyChanged();
}
}
My Service Class
private readonly IApplicantRepository _repository;
public ApplicantService()
{
_repository = new ApplicantRepository();
}
public ObservableCollection<ApplicantModel> GetAll()
{
return _repository.GetAll();
}
And my Repository
public ObservableCollection<ApplicantModel> GetAll()
{
var result =
Db.Query<ApplicantModel, AddressModel, GraduationModel, GenderModel, GradeModel, ApplicantModel>(
"SELECT a.ID, a.FirstName, a.LastName, a.Birthday, a.EMail, a.Job, ad.PostCode, ad.HomePlace, g.Graduation, g.ID, ge.Gender, ge.ID, Grade.Grade FROM dbo.Applicant As a INNER JOIN Address As ad ON ad.PostCode = a.PostCode INNER JOIN Graduation As g ON g.ID = a.GraduationID INNER JOIN Gender As ge On ge.ID = a.GenderID INNER JOIN Grade ON Grade.Grade = a.GradeID",
_mapSelectGetAll, splitOn: "HomePlace,Graduation,Gender,Grade");
Logger.LogInfo("Function GetAll executed in ApplicantRepository!");
ObservableCollection<ApplicantModel> obsCollection = new ObservableCollection<ApplicantModel>(result);
return obsCollection;
}
private ApplicantModel _mapSelectGetAll(ApplicantModel applicant, AddressModel address, GraduationModel graduation, GenderModel gender, GradeModel grade)
{
applicant.Graduation = graduation;
applicant.Address = address;
applicant.Gender = gender;
applicant.Grade = grade;
return applicant;
}
I hope anyone can show me, where the mistake is.
Your analysis is spot on :) It is indeed a problem with your binding to AllData, mainly because you allow it to be reassigned in a setter.
Your DataGrid is really only listening to the changes of your ObservableCollection (or rather, handling the INotifyCollectionChanged interface. It doesn't know that your parent object has changed the collection itself (even though you fire a PropertyChanged event)
Ideally, you should auto-initialize observable collections, and not allow any outside setting of it. Let it handle addition/removal/resetting through the CollectionChanged.
The good thing is that you could now write your program in an easier way (no need to pass around ObservableCollection<T>)
So your viewmodel property would go to:
public IList<ApplicantModel> AllData { get; } = new ObservableCollection<ApplicationModel>();
because ObservableCollection<T> inherits from IList<T> there is no problem in dumming it down. The DataGrid will still detect the INotifyCollectionChanged interface.
This does mean however that you can no longer assign a new collection to the property, but that's in essence a good thing :) Also, the AllData property will never be null, so that's another good thing.
However, you now need to rewrite the logic of updating your AllData property on your ViewModel
So, in case you still want to auto-fill it in the constructor, you can change the viewmodel property and constructor to:
public BaseViewModel() {
AllData = new ObservableCollection<ApplicantModel>( ApplicantService.GetInstance().GetAll() );
}
public IList<ApplicantModel> AllData { get; }
And no need for the private backing field, you now have a readonly auto-property instead, and the constructor utilizes the IEnumerable<T> constructor parameter for the ObservableCollection<T>
Then, you should change your service class, there is really no reason for it to return a full blown ObservableCollection<ApplicantModel>(), just let it return IEnumerable<ApplicantModel> or at most a IReadOnlyList<ApplicantModel>.
This change then also needs to go to your Repository.
This then eventually leaves you to handle deletions and updates of data. If you are working with some local database, just acting upon save/delete requests would be fine, if you work with a central database, it might be more involving :)
Hopefully it's not to much text :)
#Icepickle
My Insert query is:
public void CreateApplicant(ApplicantModel applicant, AddressModel address, GraduationModel graduation, GenderModel gender, GradeModel grade)
{
string query = "INSERT INTO Applicant(FirstName,LastName,PostCode,Birthday,GenderID,Email,Job,GraduationID,GradeID)" + "Values(#FirstName,#LastName,#PostCode,#Birthday,#Gender,#Email,#Job,#Graduation,#Grade)";
try
{
Db.Execute(query, new
{
FirstName = applicant.FirstName,
LastName = applicant.LastName,
Birthday = applicant.Birthday,
PostCode = applicant.PostCode,
Graduation = graduation.ID,
Gender = gender.ID,
Email = applicant.EMail,
Job = applicant.Job,
Grade = grade.Grade,
});
Logger.LogInfo("Function AddApplicant executed in ApplicantRepository!");
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}
And the different details are filled into textboxes which are bound to the properties of my database

WPF MVVM Datagrid Cell Validation Error Behavior Issue: Why is my value being removed?

My issue is the behavior of the validation errors in the DataGrid. It validates against my model object's property and displays the correct message, but the validation disappears, along with the original value whenever I select a different row.
In the example .gif below, I remove the name (backspace), hit enter (get the validation message), then click to a different row. Whenever the selected row changes I would expect either a) the validation error to remain OR b) the original value to return, but the row stays blank and the validation error is gone until I double click the row. Once I double click, the original value returns.
I would prefer the validation error to persist, but I'll take either at this point.
Here is the datagrid textblock style:
<Style x:Key="datagridElemStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Yellow" />
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Here is the actual DataGridTextColumn:
<DataGridTextColumn Header="NAME"
Width="300"
Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus,
ValidatesOnExceptions=True}"
ElementStyle="{StaticResource datagridElemStyle}"
CanUserReorder="False" />
This is my ViewModel wrapper object (irrelevant parts omitted):
public class PointVM : INotifyPropertyChanged
{
public Point DataContext { get; set; }
public string Name
{
get { return DataContext.Name; }
set
{
if (value != DataContext.Name)
{
DataContext.Name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
}
And finally, here is my Model (irrelevant parts omitted):
public abstract class Point
{
private string _name;
public string Name
{
get { return _name; }
set
{
string trimmedVal = value.Trim();
#region Validation
if (string.IsNullOrEmpty(trimmedVal))
throw new Exception("Name cannot be empty.");
if (Regex.IsMatch(trimmedVal, #"[^A-Za-z0-9\-_ ]$"))
throw new Exception("Invalid character in name.");
if (trimmedVal.Length > 64)
throw new Exception("Name is too long.");
if ((from p in PointList
where p.Name.Equals(trimmedVal, StringComparison.OrdinalIgnoreCase)
select p).Count() > 0)
throw new Exception("Name is already used.");
#endregion
_name = trimmedVal;
}
}
Thanks for your time.
DataGrid validations can be super annoying. When a DataGridTextColumn begins editing, the actual value of the binding is restored, which is why you see a revert when you enter edit mode.
You basically need to stop that call by handling the BeginningEdit event for the DataGrid.
<DataGrid AutoGenerateColumns="False" BeginningEdit="dg_BeginningEdit">
Code-Behind
private void dg_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
e.EditingEventArgs.Handled = true;
}
FYI: Your binding updates on LostFocus. This solution may not work well if you update on PropertyChanged in the future as it changes the order of operation.

WPF checkbox all not not updating on one checkbox row is unchecked

I had a checkbox all column inside the datagrid in WPF C#.
<DataGridCheckBoxColumn Binding="{Binding IsSelected,UpdateSourceTrigger=PropertyChanged}" CanUserSort="False">
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridCheckBoxColumn.ElementStyle>
<DataGridCheckBoxColumn.HeaderTemplate>
<DataTemplate x:Name="dtAllChkBx">
<CheckBox Name="cbxAll" HorizontalAlignment="Center" Margin="0,0,5,0" IsEnabled="{Binding Path=DataContext.IsCbxAllEnabled,RelativeSource={RelativeSource AncestorType=DataGrid}}"
IsChecked="{Binding Path=DataContext.AllSelected,RelativeSource={RelativeSource AncestorType=DataGrid},UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridCheckBoxColumn.HeaderTemplate>
</DataGridCheckBoxColumn>
When I check the All checkbox, of course, it will mark all the checkboxes, but once I uncheck one checkbox, the All checkbox is still checked. This should be unchecked. How should I do that using WPF C#.
If I understood you correctly - after any change of IsSelected property inside collection item you should update AllSelected value.
So, you need some callback inside all your items(event or Action or any mechanism you want) and change get logic for AllSelected
Here is some draft for item IsSelected property and constructor:
public bool IsSelected {
get { return isSelected; }
set {
isSelected = value;
OnPropertyChanged();
if (globalUpdate != null) globalUpdate();
}
}
public ItemClass(Action globalUpdate, ...your parameters) {
this.globalUpdate = globalUpdate;
...do smth with your parameters
}
Example of usage:
new ItemClass(() => OnPropertyChanged("AllSelected"))
And of course don't forget about AllSelected getter
public bool AllSelected {
get { return YourGridItemsCollection.All(item => item.IsSelected); }
Now when you check manually all items then AllSelected will be automatically checked, and unchecked when you uncheck any item.

DataGrid Select Entire Column

I am using WPF DataGrid and I would like to allow the user to select an entire column by clicking on the column header. I currently have the SelectionUnit set to CellOrRowHeader because I want the same functionality for rows (which works great). Is there an easy way to also allow selection of columns by clicking the column header?
You have many options. one is create your own Template for DataGrid's Header Style. Inside the DataTemplate tag you can change the template of the headers. (you can replace the Button with a TextBlock or anything you want.)
<DataGrid>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader" >
<Setter Property="Foreground" Value="Blue" />
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate>
<Button Content={Binding Content}" MouseDown="mouseDownEventHandler">
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
or if you are using DataGrid.Columns to populate your columns and you need to set each of them separately, you can use this:
<DataGrid>
<DataGrid.Columns>
<DataGridTextColumn HeaderTemplate="{StaticResource MyTemplate1"/>
<DataGridHyperlinkColumn HeaderTemplate="{StaticResource MyTemplate2"/>
</DataGrid.Columns>
</DatGrid>
where MyTemplate1 and 2 should be defined previously in your control's Resources.
Edit:
The other approach according to this link is to add a PreviewMouseDown to your DataGrid and then find out if the mouse down was happend on the header or not.
This is the simple version of her event handler:
DependencyObject dep = (DependencyObject)e.OriginalSource;
while ((dep != null) && !(dep is DataGridColumnHeader))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null) return;
if (dep is DataGridColumnHeader)
{
MessageBox.Show(((DataGridColumnHeader)dep).Content.ToString());
}
You could also modify the ColumnHeaderStyle:
In XAML:
<DataGrid>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<EventSetter Event="Click" Handler="DataGridColumnHeader_OnClick"></EventSetter>
</Style>
</DataGrid.ColumnHeaderStyle>
</DataGrid>
In the code-behind:
private void DataGridColumnHeader_OnClick(object sender, RoutedEventArgs e)
{
var columnHeader = sender as DataGridColumnHeader;
if (columnHeader != null)
{
if (!Keyboard.IsKeyDown(Key.LeftCtrl))
{
dataGrid.SelectedCells.Clear();
}
foreach (var item in dataGrid.Items)
{
dataGrid.SelectedCells.Add(new DataGridCellInfo(item, columnHeader.Column));
}
}
}
In WPF there is no direct support for column selection so, need to Extend existing DataGrid and add custom support for Column Selection.
Below is the code that will allow used to select column on Header click.
For Below code to work following setting is required.
Xaml Code:
<local:DataGridEx SelectionUnit="CellOrRowHeader" CanUserSortColumns="False"/>
C# Code:
public class DataGridEx : DataGrid
{
/// <summary>
/// Holds the column that is selected.
/// </summary>
public object SelectedColumn
{
get { return GetValue(SelectedColumnProperty); }
set { SetValue(SelectedColumnProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedColumn. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedColumnProperty =
DependencyProperty.Register("SelectedColumn", typeof(object),
typeof(DataGridEx), new PropertyMetadata(null));
private T GetVisualParent<T>(DependencyObject child) where T : Visual
{
DependencyObject parent = VisualTreeHelper.GetParent(child);
if (parent == null || parent is T)
{
return parent as T;
}
else
{
return GetVisualParent<T>(parent);
}
}
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
DataGridColumnHeader dataGridColumnHeader = GetVisualParent<DataGridColumnHeader>(e.OriginalSource as DependencyObject);
if (dataGridColumnHeader == null)
{
return;
}
if (SelectedCells != null && SelectedCells.Count > 0)
{
UnselectAllCells();
SelectedCells.Clear();
}
SelectedColumn = dataGridColumnHeader.Column;
foreach (var item in this.Items)
{
this.SelectedCells.Add(new DataGridCellInfo(item, dataGridColumnHeader.Column));
}
base.OnPreviewMouseDown(e);
}
}

Categories