Silverlight Datagrid Row Click - silverlight

I have a datagrid with a column containing a checkbox. I want to change the value of the bound Selected property when the row is clicked:
alt text http://lh4.ggpht.com/_L9TmtwXFtew/Sw6YtzRWGEI/AAAAAAAAGlQ/pntIr2GU6Mo/image_thumb%5B3%5D.png
NOTE: I don't want to use the SelectedItemChanged event because this doesn't work properly when there is only one row in the grid.

As is often the way i have found my own solution for this:
Add a MouseLeftButtonUp event to the datagrid:
<data:DataGrid x:Name="dgTaskLinks"
ItemsSource="{Binding TaskLinks}"
SelectedItem="{Binding SelectedTaskLink, Mode=TwoWay}"
MouseLeftButtonUp="dgTaskLinks_MouseLeftButtonUp"
>...
And walk the visual tree to get the data grid row:
private void dgTaskLinks_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
///get the clicked row
DataGridRow row = MyDependencyObjectHelper.FindParentOfType<DataGridRow>(e.OriginalSource as DependencyObject);
///get the data object of the row
if (row != null && row.DataContext is TaskLink)
{
///toggle the IsSelected value
(row.DataContext as TaskLink).IsSelected = !(row.DataContext as TaskLink).IsSelected;
}
}
Once found, it is a simple approach to toggle the bound IsSelected property :-)
Hope this helps someone else.

Here is an even simpler solution
XAML
<data:DataGrid
x:Name="dgMyDataGrid"
ItemsSource="{Binding MyList}"
SelectedItem="{Binding MyList, Mode=TwoWay}"
MouseLeftButtonUp="dgMyDataGrid_MouseLeftButtonUp">...
CS
private void dgMyDataGrid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
DataGrid dg = (sender as DataGrid);
var allObjects = dg.DataContext as List<MyCustomObject>;
foreach(var o in allObjects)
{
o.Selected = false;
}
MyCustomObject SelectedObject = (MyCustomObject)dg.SelectedItem;
SelectedObject.Selected = true;
}
Note: this as well as the other example assumes your class that you are binding to the control implements INotifyPropertyChanged

Related

How can I get a DataGrid to unselect on click when SelectionMode="Extended"?

The default behaviour of a WPF DataGrid is to select when a row is clicked if SelectionMode="Extended" which is what I want, however I also wish for the row to un-select if it was previously already selected when clicked.
I have tried the following which will unselect the row as soon as it's selected, it seems the row selection occurs before the mouse click event.
private void DoGridMouseLeftButtonUp(object sender, MouseButtonEventArgs args) {
// Get source row.
DependencyObject source = (DependencyObject)args.OriginalSource;
var row = source.FindParent<DataGridRow>();
if (row == null)
return;
// If selected, unselect.
if (row.IsSelected) {
row.IsSelected = false;
args.Handled = true;
}
}
Where I am binding to this event with the following grid.
<DataGrid SelectionMode="Extended"
SelectionUnit="FullRow"
MouseLeftButtonUp="DoGridMouseLeftButtonUp">
I have managed to solve this by instead of handling events on the grid itself to handle them on the cell instead, this involves an event setter for DataGridCell as follows:
<DataGrid SelectionMode="Extended"
SelectionUnit="FullRow">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}">
<EventSetter Event="PreviewMouseLeftButtonDown"
Handler="DoCheckRow"/>
</Style>
</DataGrid.Resources>
<!-- Column mapping omitted. -->
</DataGrid>
Event handler code.
public void DoCheckRow(object sender, MouseButtonEventArgs e) {
DataGridCell cell = sender as DataGridCell;
if (cell != null && !cell.IsEditing) {
DataGridRow row = FindVisualParent<DataGridRow>(cell);
if (row != null) {
row.IsSelected = !row.IsSelected;
e.Handled = true;
}
}
}
My grid is read only so any edit behavior is ignored here.
my wpf datagrid requires CTRL+CLICK for both addition and removal of MULTIPLE rows. so its standard behavior ;) but nevertheless, why you dont use the PreviewMouseDown event and then check for leftmousebutton and Ctrl and do your unselect logic and set e.handled=true?

Expand/Collapse button in a Silverlight DataGrid

I am using a RowDetailsTemplate in a Silverlight DataGrid to show row details. Setting RowDetailsVisibilityMode="VisibleWhenSelected" does not give a good user experience (only one row can be expanded at a time, all rows cannot be collapsed). What's the easiest way to add an expand/collapse button on each row so that the rows can be expanded/collapsed independently?
I've been meaning to blog my solution to this.
I set the grid RowDetailsVisibilityMode to Collapsed and use a DataGridTemplateColumn with a styled ToggleButton in it to toggle the row visibility.
The toggle button can be wired up to toggle the row visibility using either binding or through a TriggerAction.
Binding has to be done in code-behind since you are trying to bind ToggleButton.IsChecked to an element that is generated and does not exist in XAML (DataGridRow.DetailsVisibility)
(This will be allowed in SL5 with a stronger RelativeSource binding)
For both solutions I have this extension method in a helper class:
/// <summary>
/// Walk up the VisualTree, returning first parent object of the type supplied as type parameter
/// </summary>
public static T FindAncestor<T>(this DependencyObject obj) where T : DependencyObject
{
while (obj != null)
{
T o = obj as T;
if (o != null)
return o;
obj = VisualTreeHelper.GetParent(obj);
}
return null;
}
For code-behind binding method:
private void ToggleButton_Loaded(object sender, RoutedEventArgs e)
{
ToggleButton button = sender as ToggleButton;
DataGridRow row = button.FindAncestor<DataGridRow>(); //Custom Extension
row.SetBinding(DataGridRow.DetailsVisibilityProperty, new Binding()
{
Source = button,
Path = new PropertyPath("IsChecked"),
Converter = new VisibilityConverter(),
Mode = BindingMode.TwoWay
});
}
For TriggerAction method:
public class ExpandRowAction : TriggerAction<ToggleButton>
{
protected override void Invoke(object o)
{
var row = this.AssociatedObject.FindAncestor<DataGridRow>();
if (row != null)
{
if (this.AssociatedObject.IsChecked == true)
row.DetailsVisibility = Visibility.Visible;
else
row.DetailsVisibility = Visibility.Collapsed;
}
}
}
Then in XAML:
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ToggleButton Style="{StaticResource PlusMinusToggleButtonStyle}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<behaviors:ExpandRowAction/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ToggleButton>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>

WPF DataGrid source updating on cell changed

I am new to the WPF ,and i use it to build a point of sale system.
I have a DataGrid control in the main window bound to an ObservableCollection of Item, the cashier will enter/scan the items to be sold the default quantity for each item is 1 but it is available for the cashier to change the quantity manually.
Whenever I change the quantity, it should update the total price with the sum of the items' prices when I leave the cell to another cell on the row, but it doesn't happen, the source is updated only when I go to another row not another cell in the same row.
Is there anyway to force the DataGrid to update the source when the cell is changed rather than the row?
Apply the UpdateSourceTrigger=LostFocus to each binding. It worked like a charm for me.
<DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
The code in the accepted answer didn't work for me since the row fetched from ItemContainerGenerator.ContainerFromItem(item) results in null and the loop be quite slow.
A more simple solution to the question is the code provided here:
http://codefluff.blogspot.de/2010/05/commiting-bound-cell-changes.html
private bool isManualEditCommit;
private void HandleMainDataGridCellEditEnding(
object sender, DataGridCellEditEndingEventArgs e)
{
if (!isManualEditCommit)
{
isManualEditCommit = true;
DataGrid grid = (DataGrid)sender;
grid.CommitEdit(DataGridEditingUnit.Row, true);
isManualEditCommit = false;
}
}
Yes, this is possible. Your question is basically the same as DataGrid - change edit behaviour
The code below is mostly from Quartermeister's answer but I added a DependencyProperty BoundCellLevel that you can set when you need a DataGrid binding to be updated when the current cell changes.
public class DataGridEx : DataGrid
{
public DataGridEx()
{
}
public bool BoundCellLevel
{
get { return (bool)GetValue(BoundCellLevelProperty); }
set { SetValue(BoundCellLevelProperty, value); }
}
public static readonly DependencyProperty BoundCellLevelProperty =
DependencyProperty.Register("BoundCellLevel", typeof(bool), typeof(DataGridEx), new UIPropertyMetadata(false));
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = base.MeasureOverride(availableSize);
if ( BoundCellLevel )
ClearBindingGroup();
return desiredSize;
}
private void ClearBindingGroup()
{
// Clear ItemBindingGroup so it isn't applied to new rows
ItemBindingGroup = null;
// Clear BindingGroup on already created rows
foreach (var item in Items)
{
var row = ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
row.BindingGroup = null;
}
}
}
Almund is right. UpdateSourceTrigger=LostFocus will work best in you case. And as you have mentioned that your source is updating when you move to next row, that means I guess, you are using ObservableCollection<T> to bind your DataGrid's ItemSource. Because that is what which you need to achieve what you want.
<DataGridTextColumn Header="Quantity" Binding="{Binding Quantity,
Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
<DataGridTextColumn Header="Total Price" Binding="{Binding TotalPrice,
Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
You need to add "UpdateSourceTrigger=LostFocus" to each of your columns.
For autogenerated columns the following code works:
private void ExcludeTable_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column is DataGridCheckBoxColumn column && column.Binding is System.Windows.Data.Binding binding)
{
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
}
}

WPF DataGrid Hide RowDetails or Unselect Row

I have a DataGrid who's RowDetails is set to show when selected (RowDetailsVisibilityMode="VisibleWhenSelected"). Now I want to be able to get rid of it! I put a close button on the row details with this code:
private void Button_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;
Button button = sender as Button;
DataGridRow row = button.FindAncestor<DataGridRow>();
row.DetailsVisibility = Visibility.Collapsed;
}
That code gets me 90% there, but once the row details is collapsed for a given row it will not appear the next time that row is selected.
I've run into this too. Here's a solution:
Keep that button in the RowDetails and change its code a little. Rather than focusing on the individual row's visibility, set the DataGrid's SelectedIndex property to -1 (none selected).
DataGrid1.SelectedIndex = -1;
Since your RowDetailsVisibilityMode is VisibleWhenSelected, the DataGrid will collapse/hide any expanded RowDetails. This works well when the SelectionMode is Single.
You can implement this with the following code in XAML:
<WpfToolkit:DataGrid Name="dgSysthetic" ItemsSource="{Binding}"
AutoGenerateColumns="True"
SelectionMode="Extended"
RowDetailsVisibilityMode="Collapsed"
CanUserAddRows="False" CanUserDeleteRows="False"
CanUserResizeRows="False" CanUserSortColumns="False"
RowHeaderWidth="20" RowHeight="25">
<WpfToolkit:DataGrid.RowHeaderTemplate>
<DataTemplate>
<Button Name="btnHideRow" Click="btnHideDetails_Click" FontSize="5">></Button>
</DataTemplate>
</WpfToolkit:DataGrid.RowHeaderTemplate>
<WpfToolkit:DataGrid.RowDetailsTemplate>
<DataTemplate>
<WpfToolkit:DataGrid Name="dgAnalytical" ItemsSource="{Binding}" AutoGenerateColumns="True"/>
</DataTemplate>
</WpfToolkit:DataGrid.RowDetailsTemplate>
</WpfToolkit:DataGrid>
See the button inside RowHeaderTemplate.
In your C# code you would do this:
private void btnHideDetails_Click(object sender, RoutedEventArgs e)
{
DependencyObject obj = (DependencyObject)e.OriginalSource;
while (!(obj is DataGridRow) && obj != null) obj = VisualTreeHelper.GetParent(obj);
if (obj is DataGridRow)
{
if ((obj as DataGridRow).DetailsVisibility == Visibility.Visible)
{
(obj as DataGridRow).DetailsVisibility = Visibility.Collapsed;
}
else
{
(obj as DataGridRow).DetailsVisibility = Visibility.Visible;
}
}
}
This worked very well for me.
you can put this as your buttons click event, it walks up the tree finds the datarow and sets the details where needed.
DependencyObject dep = (DependencyObject)e.OriginalSource;
while ((dep != null) && !(dep is DataGridRow))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep != null && dep is DataGridRow)
{
DataGridRow row = dep as DataGridRow;
if (row.DetailsVisibility == Visibility.Collapsed)
{
row.DetailsVisibility = Visibility.Visible;
}
else
{
row.DetailsVisibility = Visibility.Collapsed;
}
}
try adding row.DetailsVisibility = Visibility.Visible; on the RowDetailsVisibilityChanged event.
Try setting a Style on the Button with Setters that set the Button's Command, CommandParameter properties. You'll need to create your a class that inplements ICommand and include it as a StaticResource in XAML. Here I used the DataGridRowHeader as a button instead of a button within row details.
<local:DeselectRowCommand x:Key='deselectCommand' />
<Setter Property='Command' Value='{StaticResource deselectCommand}' />
<Setter Property='CommandParameter'
Value='{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=wpf:DataGridRow}}' />
In the command's Execute method you can get the DataGridRow from the command parameter and apply whatever methods you need to.
At least this way you can share this Style or base others off of it and re-use the ICommand for your other DataGrids, and also less event handling.
You can see a working example in this Silverlight-to-WPF DataGrid open-source project.
Try this (adding the PreviewMouseDown event to your DataGrid in XAML):
private void UIElement_OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
DataGrid grid = sender as DataGrid;
if (grid != null)
{
FrameworkElement element = e.OriginalSource as FrameworkElement;
if (element?.DataContext is FixedIncomeOrder)
{
if (grid.SelectedItem == (FixedIncomeOrder) ((FrameworkElement) e.OriginalSource).DataContext)
{
grid.SelectedIndex = -1;
e.Handled = true;
}
}
}
}
Make sure your datagrid has a name such as
<DataGrid x:Name="dgPrimary"
...>
Place a button in the row template such as
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<Button Content="X" Click="Button_Click" Width="20"/>
....
Then in the codebehind simply set the datagrid's selected index to -1
private void Button_Click(object sender, RoutedEventArgs e)
{
dgPrimary.SelectedIndex = -1;
}

ComboBox with ItemTemplate that includes a button

So, lets say I have a ComboBox with a custom data template. One of the items in the data template is a button:
<ComboBox Width="150" ItemsSource="{Binding MyItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Button Content="ClickMe" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The problem with this is that the button eats the click, and the item does not get selected if the button is selected. This means that the pull-down does not go away, and no item is selected.
I get WHY this is happening.
Is there a way to work around it? Possibly a way to process the button click (I am binding to a command) and tell it to continue up the chain so the combo box can also process the click?
Note: I am seeing my problem in Silverlight, but I am guessing that the exact same behavior can be seen with WPF.
OK, I got it figured out. It is a total hack, but it still lets me bind my command to the button and continue to have Combo-box behavior for selecting the item:
<ComboBox x:Name="MyCombo" Width="150" ItemsSource="{Binding MyItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Button Content="ClickMe" Click="Button_Click" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
And in the code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
MyCombo.SelectedItem = (sender as Button).DataContext;
MyCombo.IsDropDownOpen = false;
}
If I really wanted to, I could bind the SelectedItem and IsDropDownOpen to properties in my ViewModel but I decided against it to keep this behavior as a hack extension of the XAML, in an effort to keep my ViewModel clean.
Your best bet would probably be to set the SelectedItem in the button's command.
I found another possibility for the MVVM context. I used an derived class for ComboBox and if an item is adden which derives from ButtonBase I attach to the Click event to close the ComboBox.
This works for my project - but just, because the items itself are buttons, it would not work if they just contain buttons as a child element.
public class MyComboBox : ComboBox
{
public MyComboBox()
{
// use Loaded event to modify inital items.
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
if (Items != null)
{
foreach (var item in Items)
{
var button = item as ButtonBase;
if (button != null)
{
ModifyButtonItem(button);
}
}
}
}
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
// Check added items. If an item is a button, modify the button.
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
var button = item as ButtonBase;
if (button != null)
{
ModifyButtonItem(button);
}
}
}
}
private void ModifyButtonItem(ButtonBase button)
{
button.Click += (sender, args) => { IsDropDownOpen = false; };
}
}
I don't know if there is a way to do what you want. If you were to put a Button in a ListBox, for example, the same behavior occurs - clicking the Button does not cause its item in the ListBox to be selected. In fact, this is the case for any control in an ItemsControl that supports selection.
You might be able to do something with the Click event and mark it as not handled so that it continues up the visual tree, but even then I'm not sure if that would work or not.

Resources