Datagrid Column Collection Changed Event - wpf

I use this code to add or remove column from datagrid. each column header I have mouse enter and leave event. For new column I also would like to add the same event handler after inserting to datagrid.
private void Columns_CollectionChanged(object sender, System.ComponentModel.CollectionChangeEventArgs e)
{
if (e.Action == CollectionChangeAction.Add)
{
int columnPosition = (this.Columns.Count - 1);
DataGridTextColumn column = new DataGridTextColumn();
column.Header = (e.Element as DataColumn).ColumnName;
column.Binding = new Binding(string.Format("[{0}]", column.Header.ToString()));
this.Columns.Insert(columnPosition, column);
DataGridColumnHeader columnHeader = DataGridHelper.GetColumnHeader(this, columnPosition);
if (columnHeader != null)
{
columnHeader.MouseEnter += new MouseEventHandler(ColumnHeader_MouseEnter);
columnHeader.MouseLeave += new MouseEventHandler(ColumnHeader_MouseLeave);
}
SetAutomappingOnOff = false;
}
else if (e.Action == CollectionChangeAction.Remove)
{
DataColumn column = e.Element as DataColumn;
DataGridColumn toRemove = (from DataGridColumn dc in this.Columns
where dc.Header != null && dc.Header.ToString() == column.ColumnName
select dc).First();
this.Columns.Remove(toRemove);
SetAutomappingOnOff = false;
}
}
< Edit>
DataGridHelper
public static class DataGridHelper
{
public static DataGridColumnHeader GetColumnHeader(DataGrid dataGrid, int index)
{
DataGridColumnHeadersPresenter presenter = FindVisualChild<DataGridColumnHeadersPresenter>(dataGrid);
if (presenter != null) {
return (DataGridColumnHeader)presenter.ItemContainerGenerator.ContainerFromIndex(index)‌​;
}
return null;
}
}
< /Edit>
But columnHeader always returns null even though I can see that object is created and added to datagrid.
Pls help me.
Thanks
Dee

While the column has been added to the DataGrid, it hasn't yet been added to the VisualTree so your FindVisualChild method is returning null. I don't have a good solution for adding the click handler for the column, but you could add it to the DataGrid and check the sender to see where to apply the click handling logic.

I would suggest registering CollectionChanged event on DataGrid-s Loaded event. That way you can be sure that DataGridColumnHeader is added to the visual tree. It will look like this:
myDataGrid.Loaded += (s,e) => {
myCollection.CollectionChanged += (se, ev) => {
//do work here
};
};

Related

WPF DataGrid pressing enter on first cell it is adding new row I want it to add new row on las cell pressing enter

WPF Datagrid it is adding new row while pressing enter on first cell I want it to add new row once I press last cell of Datagrid.
Please check demo app is here:
WPFDemo
Thank you,
Jitendra Jadav
The DataGrid also adds a new row even when you don't even press ENTER. If you don't want this behaviour, you would probably be better of setting the CanUserAddRows property to false and add the items yourself to the source collection. Something like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dataGrid.CanUserAddRows = false;
//add blank row
var itemsSource = dataGrid.ItemsSource as ObservableCollection<ItemModel>;
if (itemsSource != null)
itemsSource.Add(new ItemModel());
Loaded += MainWindow_Loaded;
dataGrid.PreviewKeyDown += DataGrid_PreviewKeyDown;
}
private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Enter)
{
if (Keyboard.FocusedElement is UIElement elementWithFocus)
{
if (dataGrid.Columns.Count - 1 == dataGrid.CurrentCell.Column.DisplayIndex)
{
var itemsSource = dataGrid.ItemsSource as ObservableCollection<ItemModel>;
if (itemsSource != null)
{
var newItem = new ItemModel();
itemsSource.Add(newItem);
dataGrid.SelectedItem = newItem;
Dispatcher.BeginInvoke(new Action(()=>
{
DataGridRow row = dataGrid.ItemContainerGenerator.ContainerFromItem(newItem) as DataGridRow;
DataGridCell cell = Helper.GetCell(dataGrid, row, 0);
if (cell != null)
dataGrid.CurrentCell = new DataGridCellInfo(cell);
}), DispatcherPriority.Background);
}
}
else
{
elementWithFocus.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
e.Handled = true;
}
}
}
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Helper.SelectRowByIndex(dataGrid, 0);
}
}

Searching In DataGrid

I want to search in a data grid via typing in a textbox, but I am unable to find solution.
Do I need to do any binding? If so, then how do I do it?
If you want filter text in your Datagrid i.e by Name, try this...
private bool DataMatchesFilterText(User user, string filterText)
{
return user.Name.ToString() == filterText;
}
Yeah you will require your data grid to be bound to a Property that contains all your data.
Then add a event handler to your Textbox to act on one of the key events, e.g.
Xaml:
<TextBox x:Name="SearchBox" KeyUp="FilterTextBox_TextChanged" />
Then in the code behind you need to act on that event. Here you need to extract the filter text, get the rows in your DataGrid and then perform some method to determine if it should be visible or not. You will need to implement your own DataMatchesFilterText method.
Codebehind:
private void FilterTextBox_TextChanged(object sender, KeyEventArgs e)
{
var filterTextBox = (TextBox)sender;
var filterText = filterTextBox.Text;
SetRowVisibilityByFilterText(filterText);
}
private void SetRowVisibilityByFilterText(string filterText)
{
GetVisibleRows(yourGrid)
.ToList()
.ForEach(
x =>
{
if (x == null) return;
x.Visibility =
DataMatchesFilterText(x.Item as YourRowProperty, filterText) ? Visibility.Visible : Visibility.Collapsed;
});
}
public static IEnumerable<DataGridRow> GetVisibleRows(DataGrid grid)
{
if (grid == null || grid.Items == null) yield break;
int count = grid.ItemsSource == null
? grid.Items.Count
: grid.ItemsSource.Cast<object>().Count();
for (int i = 0; i < count; i++)
{
yield return (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(i);
}
}

GridvView.SelectedCells.Add() does not update view

GridViews Selected cells is a IList, so it does not update the view, when i add selections from my ViewModel.
Is there a way to force updating the view for selected Cells. The way i currently uddate views is by having a Attached behavior, which updates the list on ViewModel, but also the GridView, but the GridView does not update its visuals.
here is my attached behavior:
public static List<GridCell> GetSelectedCells(DependencyObject obj)
{
return (List<GridCell>)obj.GetValue(SelectedCellsProperty);
}
public static void SetSelectedCells(DependencyObject obj, List<GridCell> value)
{
obj.SetValue(SelectedCellsProperty, value);
}
public static readonly DependencyProperty SelectedCellsProperty =
DependencyProperty.RegisterAttached("SelectedCells", typeof(List<GridCell>), typeof(DataGridHelper), new UIPropertyMetadata(null, OnSelectedCellsChanged));
static SelectedCellsChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
{
return (SelectedCellsChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
}
static void SetSelectionChangedHandler(DependencyObject obj, SelectedCellsChangedEventHandler value)
{
obj.SetValue(SelectionChangedHandlerProperty, value);
}
static readonly DependencyProperty SelectionChangedHandlerProperty =
DependencyProperty.RegisterAttached(nameof(SelectedCellsChangedEventHandler), typeof(SelectedCellsChangedEventHandler), typeof(DataGridHelper), new UIPropertyMetadata(null));
private static bool NewResouce = false;
static void OnSelectedCellsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (d is DataGrid)
{
NewResouce = true;
DataGrid datagrid = d as DataGrid;
if (GetSelectionChangedHandler(d) == null)
{
SelectedCellsChangedEventHandler selectionchanged = (sender, e) =>
{
if (!NewResouce)
{
List<GridCell> cells = new List<GridCell>();
foreach (var selectedell in datagrid.SelectedCells)
{
string header = selectedell.Column.Header.ToString();
GridCell cell = new GridCell
{
RowIndex = datagrid.Items.IndexOf(selectedell.Item),
ColumnIndex = selectedell.Column.DisplayIndex,
Parent = selectedell.Item as ExpandoObject,
ColumnHeader = header,
Value = (selectedell.Item as IDictionary<string, object>)[header]
};
cells.Add(cell);
}
SetSelectedCells(d, cells);
}
};
SetSelectionChangedHandler(d, selectionchanged);
datagrid.SelectedCellsChanged += GetSelectionChangedHandler(d);
}
foreach (var selected in GetSelectedCells(d) as List<GridCell>)
{
DataGridCellInfo cell = new DataGridCellInfo(selected.Parent, datagrid.Columns[selected.ColumnIndex]);
if (!datagrid.SelectedCells.Contains(cell))
{
datagrid.SelectedCells.Add(cell);
}
}
NewResouce = false;
}
}
}
The reason why i have the NewResource boolean, is that the event selection changed does actually fire when I add newly selected items. Its just the view that does not update its selections.
The SelectedCells is added after the view is loaded, due to its located inside a tab, and it looks like the data on gridview is empty before view is loaded, so I cannot set selected before the view is loaded.
Answer is simple. Please use an ObservableCollection instead of the list.
The observable collection is a dependency object and it will raise a propertyChanged event to the view to notify it regarding the property change and the view will be updated.
I ended up finden some properties that does implement INotifyPropertyChanged, on DataGridCells heres the solution:
static void OnSelectedCellsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (d is DataGrid)
{
DataGrid datagrid = d as DataGrid;
if (GetSelectionChangedHandler(d) == null)
{
SelectedCellsChangedEventHandler selectionchanged = (sender, e) =>
{
List<GridCell> cells = new List<GridCell>();
foreach (var selectedell in datagrid.SelectedCells)
{
string header = selectedell.Column.Header.ToString();
GridCell cell = new GridCell
{
RowIndex = datagrid.Items.IndexOf(selectedell.Item),
ColumnIndex = selectedell.Column.DisplayIndex,
Parent = selectedell.Item as ExpandoObject,
ColumnHeader = header,
Value = (selectedell.Item as IDictionary<string, object>)[header]
};
cells.Add(cell);
}
SetSelectedCells(d, cells);
};
SetSelectionChangedHandler(d, selectionchanged);
datagrid.SelectedCellsChanged += GetSelectionChangedHandler(d);
}
foreach (var selected in GetSelectedCells(d) as List<GridCell>)
{
DataGridCell actualCell = datagrid.GetCell(selected.RowIndex, selected.ColumnIndex);
actualCell.IsSelected = true;
actualCell.Focus();
}
}
}
for it to work i added some extention method to datagrid, to make it easier to get the Cell here the extention methodes: (stolen from this blog)
public static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
public static DataGridCell GetCell(this DataGrid grid, int rowIndex, int columnIndex)
{
return GetCell(grid, GetRow(grid, rowIndex), columnIndex);
}
public static DataGridCell GetCell(this DataGrid grid, DataGridRow row, int column)
{
if (row != null)
{
DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row);
if (presenter == null)
{
grid.ScrollIntoView(row, grid.Columns[column]);
presenter = GetVisualChild<DataGridCellsPresenter>(row);
}
DataGridCell cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(column);
return cell;
}
return null;
}
public static DataGridRow GetRow(this DataGrid grid, int index)
{
DataGridRow row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
if (row == null)
{
// May be virtualized, bring into view and try again.
grid.UpdateLayout();
grid.ScrollIntoView(grid.Items[index]);
row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}

In Silverlight Datagrid, How to add a new row when focus goes away from Last Row?

I want to add a new row in my Silverlight DataGrid, when user try to go from LastRow to NextRow by Tab/Enter (as it last row, DataGrid loses focus). I can not use RowEditEnded event as it will fire even if i move to a PreviousRow from LastRow.
Can anyone help me achieve this?
If you look at DataGrid source code you can see that it traps key down event (f.i. to realize functionality like go to next row on enter pressed). As solution I propose to implement own grid inherited from DataGrid and add event which raised when user presses enter(or other) button. Own control:
public class MyDataGrid : DataGrid
{
public event EventHandler OnLastRowEnterPressed;
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (ItemsSource != null
&& ItemsSource.Cast<object>().Count() - 1 == SelectedIndex
&& e.Key == Key.Enter)
{
RaiseLastRowEnterPressed();
}
}
private void RaiseLastRowEnterPressed()
{
if (OnLastRowEnterPressed != null)
OnLastRowEnterPressed(this, EventArgs.Empty);
}
}
Using:
ObservableCollection<Foo> source = new ObservableCollection<Foo>()
{
new Foo(), new Foo(), new Foo(),
};
myDataGrid.OnLastRowEnterPressed += (s, e) => source.Add(new Foo());
myDataGrid.ItemsSource = source;
well vladimir, there seems to be no simple/direct way to add new row after last row exit. your solution will work but with consequences of event being raised in other key press events also. i have come up with the combination of events to get the solution.
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
addFlag = (e.Key == Key.Tab);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
addFlag = (addFlag && true);
base.OnLostFocus(e);
}
protected override void OnRowEditEnded(DataGridRowEditEndedEventArgs e)
{
base.OnRowEditEnded(e);
addFlag = (addFlag && IsLastRowSelected);
if (addFlag)
AddItem();
addFlag = false;
}
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
addFlag = false;
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
addFlag = false;
}
private void AddItem()
{
if (RaiseAddEvent!= null)
{
this.Focus();
RaiseAddEvent(this, EventArgs.Empty);
this.UpdateLayout();
this.CurrentColumn = this.Columns[0];
this.BeginEdit();
}
}
You can use Routed events concept where trapping the Enter/Tab key , you can add new row to the data grid control.
I will expose by few steps. So lets start now..
1)Declare your event in constructor of this class.
this.DataGrid1.KeyDown += new KeyEventHandler(DataGrid1_KeyDown);
you also can it in XAML file.
...KeyDown="DataGrid1_KeyDown".....
2) Go to your keydown event & wrie the code.
var focusedElement = FocusManager.GetFocusedElement();
DataGrid detailsDataGrid = sender as DataGrid;
int dataGridrows = detailsDataGrid.ItemsSource.OfType<object>().Count();
if (e.Key == Key.Tab && (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
return;
if (e.Key == Key.Tab)
try
{
detailsDataGrid.SelectedIndex = row.GetIndex();
{
itemMaster.TransactionChilds.Add(transactionChild);
detailsDataGrid.SelectedItem = transactionChild;
}
}
3) Now code line by line..
DataGridRow row = DataGridRow.GetRowContainingElement(focusedElement as FrameworkElement);
DataGridColumn column = DataGridColumn.GetColumnContainingElement(focusedElement as FrameworkElement);
TransactionMaster itemMaster = this.DataFormVoucher.CurrentItem as TransactionMaster;
decimal serialNumber = 0;
if (buttonPress == "Modify")
if (dataGridrows - 1 == detailsDataGrid.SelectedIndex && column.DisplayIndex == 5)
TransactionChild transactionChild = new TransactionChild()"[None]",DateTime.Now.Date,catch (Exception ex)Console.WriteLine(ex.Message);
.DataGridChild.KeyDown += new KeyEventHandler(DataGridChild_KeyDown);
3) Now understand the code line by line
i) first 3 lines are used to take which row of a datagrid is selected.
ii)When new row will add in this case i have used Tab key you can also change this.Another things is if an user predd Tab+Shift then it will go through (default as control focus).
iii) then check is it last row & last column of this grid, if yes then add new row or else.
iv) to add a blank new row just pass your object (EDMX Model Table)

WPF DataGrid doesn't shrink when column width shrinks

I am using a DataGrid in WPF and want it to shrink to only fit the width of its columns. It does this nicely for the initial rendering. When I resize a column to make it wider the grid grows as well. But if I resize the column to make it narrower again I get white space on the right side of my column (and I can see that the column header grey area is extended beyond the columns.
I would like to have the data grid shrink its width with the columns so I don't get the white space on the right. I have tried to debug the code and as far as I can see the problem is in the DataGridCellsPanel, but I can't see anyplace to fix the width measurement.
Any help would be appreciated.
I had that problem to a while back and I was getting so annoyed by it that I made an ugly fix for it. It's not pretty, but it gets the job done. First, this is only a problem when the Horizontal ScrollBar is invisible so we're gonna need a reference to it. This code will have to be run once all DataGridColumns have been loaded (in my case, all in Xaml, so the Loaded event) and it doesn't take adding/removing of DataGridColumns into consideration but that's an easy fix.
<DataGrid Name="c_dataGrid"
Loaded="c_dataGrid_Loaded"
...>
<DataGrid.Columns>
<DataGridTextColumn ..."/>
<DataGridTextColumn ..."/>
<!-- ... -->
Then in the Loaded EventHandler we get the DataGrid ScrollViewer and add a listener for changes in the ActualWidthProperty of every DataGridColumn in the DataGrid.
private ScrollViewer m_dataGridScrollViewer = null;
private void c_dataGrid_Loaded(object sender, RoutedEventArgs e)
{
m_dataGridScrollViewer = GetVisualChild<ScrollViewer>(c_dataGrid);
DependencyPropertyDescriptor dependencyPropertyDescriptor =
DependencyPropertyDescriptor.FromProperty(DataGridColumn.ActualWidthProperty, typeof(DataGridColumn));
if (dependencyPropertyDescriptor != null)
{
foreach (DataGridColumn column in c_dataGrid.Columns)
{
dependencyPropertyDescriptor.AddValueChanged(column, DataGridColumn_ActualWidthChanged);
}
}
}
And then we compute the size of the DataGrid from the size of all DataGridColumns and add a constant of 8.0 (which is the difference normally).
private void DataGridColumn_ActualWidthChanged(object sender, EventArgs e)
{
if (m_dataGridScrollViewer != null)
{
if (m_dataGridScrollViewer.ComputedHorizontalScrollBarVisibility != Visibility.Visible)
{
double dataGridWidth = 8.0;
foreach (DataGridColumn column in c_dataGrid.Columns)
{
dataGridWidth += column.ActualWidth;
}
c_dataGrid.Width = dataGridWidth;
}
else
{
c_dataGrid.Width = double.NaN;
}
}
}
If you come up with a better way of doing this then let me know :)
public static T GetVisualChild<T>(object parent) where T : Visual
{
DependencyObject dependencyObject = parent as DependencyObject;
return InternalGetVisualChild<T>(dependencyObject);
}
private static T InternalGetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
That's a nice solution. I have tweaked it slightly so that it sets the MaxWidth property instead. This solves the problem of the grid expanding beyond the constraints of the visual parent. I also converted it into a behavior instead in order to encapsulate it better.
This is what I ended up with.
public class UpdateWidthOnColumnResizedBehavior : Behavior<DataGrid>
{
private static readonly DependencyPropertyDescriptor Descriptor;
static UpdateWidthOnColumnResizedBehavior()
{
Descriptor = DependencyPropertyDescriptor.FromProperty(DataGridColumn.ActualWidthProperty, typeof(DataGridColumn));
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Columns.CollectionChanged += OnColumnsCollectionChanged;
foreach (var column in AssociatedObject.Columns)
{
AddListener(column);
}
}
void OnColumnsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var column in e.NewItems.OfType<DataGridColumn>())
{
AddListener(column);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var column in e.OldItems.OfType<DataGridColumn>())
{
RemoveListener(column);
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var column in e.NewItems.OfType<DataGridColumn>())
{
AddListener(column);
}
foreach (var column in e.OldItems.OfType<DataGridColumn>())
{
RemoveListener(column);
}
break;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
foreach (var column in AssociatedObject.Columns)
{
RemoveListener(column);
}
}
private void AddListener(DataGridColumn column)
{
Descriptor.AddValueChanged(column, ResizeGrid);
}
private void RemoveListener(DataGridColumn column)
{
Descriptor.RemoveValueChanged(column, ResizeGrid);
}
private void ResizeGrid(object sender, EventArgs e)
{
var columnsWidth = AssociatedObject.Columns.Sum(c => c.ActualWidth);
AssociatedObject.MaxWidth = columnsWidth + 2;
AssociatedObject.InvalidateMeasure();
}
}
I still have some things to iron out about width coordination of two grids, but it looks to work for one.
there is seems to be a slight problem with both of your approaches. When I drag the left most column to the right, the whole grid is getting resized/ rolled inside (unfortunatly I don't have enough reputation to post the image).
So I have modified jjrdk ResizeGrid function, so it calculate the last column width and extends it all the way to the left. The grid HorizontalAlignment and HorizontalContentAlignment must be set to
HorizontalAlignment.Stretch.
void ResizeGrid(object sender, EventArgs e)
{
var scroll = ExTreeHelper.FindVisualChild<ScrollViewer>(AssociatedObject);
if (scroll != null && null != AssociatedObject.Columns && AssociatedObject.Columns.Count > 0)
{
var lastColumn = AssociatedObject.Columns.Last();
double dataGridWidth = AssociatedObject.Columns.Sum(c => c.ActualWidth) + 2.0;
if (scroll.ComputedHorizontalScrollBarVisibility != Visibility.Visible)
{
RemoveListener(lastColumn);
AssociatedObject.Columns.Last().Width =
AssociatedObject.Columns.Last().Width.DisplayValue + scroll.ViewportWidth - dataGridWidth;
AssociatedObject.Width = dataGridWidth + scroll.ViewportWidth - dataGridWidth;
AddListener(lastColumn);
}
else
{
AssociatedObject.HorizontalAlignment = HorizontalAlignment.Stretch;
AssociatedObject.HorizontalContentAlignment = HorizontalAlignment.Stretch;
AssociatedObject.Width = double.NaN;
}
} }
The only issue I have, is that the scroll bar is always there, even if all the columns has been fit.
There is still another issue, when all the columns are collapsed to the left, it starts flickering.
Is there anything that can be done, to really get rid of this white space?
Leon

Resources