Need help: DataGridCheckBoxColumn two-way works only one-way - wpf

I (..still learning wpf..) made a DataGrid with the first column showing that a signal is enabled or not.
In the xaml:
<DataGrid.Columns> ...
<DataGridCheckBoxColumn Width ="30" Header="" IsReadOnly="False" Binding="{Binding IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
The ItemSouce of DataGrid is correctly set and bound with the data in list of ObservableCollection<Signal> signalList. Everything is correctly shown in DataGrid. So the binding from signalList to DataGrid here is working fine.
On the other side, I want that with every change of signalList, the DataGrid can update itself automatically. However, if I do
signalList[0].IsEnabled = true;
The DataGrid doesn't get updated. I searched a lot but still can't find the answer.
Did I miss something? Thx.
Edit1:
DataGrid do get updated, only if I click another row, and then draw the scroller out of sight. Then if I draw the scroller back, the row is correctly shown. I think I definitively missed something, can someone give me a hint?

I solved the problem with help from Aybe and gavin. For record, I add my code here:
class Signal : INotifyPropertyChanged
{
...
private bool isEnabled;
public bool IsEnabled
{
get { return isEnabled; }
set { isEnabled = value; OnPropertyChanged(new PropertyChangedEventArgs("IsEnabled"));}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}

Related

DataGrid not binding (XAML + ViewModel + ObservableCollection)

New to the WPF + XAML + MVVM stack so I'm sure I'm doing something basic here, but Googling hasn't helped me figure it out. I think A second set of eyes may help.
The Setup
I have a list of Objects called FilesToAdd
I have a DataGrid bound to this list
I have a drag and drop event that fires handling code
I've confirmed this works via Console.WriteLine() output.
The Goal
When an item is added to the list, I'd like the datagrid to be updated with the appropriate information that has just been added to the list.
The Problem
The list seems to be updated, but the datagrid never is.
The Code
Showing only the relevant parts.
UploaderViewModel Class
private ObservableCollection<IAddableFile> _filesToAdd;
public event PropertyChangedEventHandler PropertyChanged;
public UploaderViewModel()
{
_filesToAdd = new ObservableCollection<IAddableFile>();
}
protected virtual void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public ObservableCollection<IAddableFile> FilesToAdd
{
get { return _filesToAdd; }
set
{
if (value != _filesToAdd)
{
_filesToAdd = value;
OnPropertyChanged("FilesToAdd");
OnPropertyChanged("FilesAreQueued");
}
}
}
public bool FilesAreQueued
{
get { return (FilesToAdd.Count > 0); }
}
public void AFileHasBeenAdded(string filepath)
{
var message = String.Format("File dropped: {0}", filepath);
Console.WriteLine(message);
var newFileInfo = new FileInfo(filepath);
if (newFileInfo.Exists && newFileInfo.Length > 0 && (!FileIsADirectory(newFileInfo))) // only add the file to the ViewModel if it's
{
FilesToAdd.Add(new FileSystemFile(newFileInfo)); //Creating our own type becaause we do additional things with it
Console.WriteLine(String.Format("File added to list: {0}", newFileInfo.FullName));
}
}
XAML Binding
<DataGrid ItemsSource="{Binding FilesToAdd}" Height="100" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" MaxHeight="100" AutoGenerateColumns="False" Visibility="{Binding FilesAreQueued, Converter={StaticResource BoolToVisConverter}}">
<DataGrid.Columns>
<DataGridTextColumn Header="File Name" Binding="{Binding FileName}"/>
<DataGridTextColumn Header="Size" Binding="{Binding FileSizeInText}"/>
</DataGrid.Columns>
</DataGrid>
What am I missing? I've been looking at the pattern and I know it has to be something simple I'm not seeing due to staring at a screen for too long. :)
Edit: I suspect the DataGrid updates just fine but you can't see it because the FilesAreQueued property is lying.
You would need something like
FilesToAdd.CollectionChanged += (s,e) =>
OnPropertyChanged("FilesAreQueued");
As you only want to do that once (if at all, can bind to FilesToAdd.Count directly), you really should opt for a readonly collection field.
Looks fine if the DataContext of the view is actually that view-model.
Another issue could be that the class is not implementing INotifyPropertyChanged (you can have the event without actually implementing it using class : interface), this would only apply if you overwrite the FilesToAdd property with a new instance. (In general i expose collections as get-only with a readonly field.)
Might want to check for binding errors (don't think you get any for bindings to a null DataContext though).
(Also i would recommend making the OnPropertyChanged thread-safe, i.e. var handler = <event>; if (handler != null) handler();)

Why does the ComboBox in my DataGrid column reset itself to null?

In my WPF application, I am developing a fairly straightforward page that allows either creating a new object or choosing one from a combo box, then editing the object.
One of the parts of the object that is editable is a related database table in a one-to-many relationship, so for that piece I used a DataGrid. The DataGrid itself has a data-bound ComboBox column, as you can see here:
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
CanUserAddRows="False" CanUserDeleteRows="True"
ItemsSource="{Binding Path=No.Lower_Assy}"
DataGridCell.Selected="dgAssy_GotFocus">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Number & Type">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=DataContext.ComboSource, RelativeSource={RelativeSource AncestorType=Page}}"
SelectedValuePath="bwk_No"
SelectedValue="{Binding Path=fwf_Higher_N, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Number}"/>
<TextBlock Text="{Binding Path=Type}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- other text columns omitted -->
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" Click="btnDeleteHigherAssy_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Code behind:
private void dgAssy_GotFocus(object sender, RoutedEventArgs e)
{
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the edit on the row
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
And for the save button:
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (CanUserEdit())
{
if (string.IsNullOrWhiteSpace(model.Data.Error))
{
repo.Save(model.Data);
StatusText = STATUS_SAVED;
model.CanSave = false;
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
// Set the combo box's selected item, in case this is a new object.
// cboNo is the main combo box on the page which allows selecting
// an object to edit
// Apparently setting SelectedItem directly doesn't work on a databound combo box
int index = model.ComboSource.ToList().FindIndex(x => x.bwk_No == model.Data.bwk_No);
cboNo.SelectedIndex = index;
}
else
{
MessageBox.Show("Invalid data:\n" + model.Data.Error, "Cannot save");
}
}
}
The problem
When I choose an item from the combo box in the data grid, it seems to work until I click on the save button. Then two things happen:
The combo box's selected item is set to null, blanking out the combo box.
As a result of (1), the save button is re-enabled because the data has changed. (The save button is bound to model.CanSave, which as you can see is set to false in the button handler; it is set to true by a property change event handler if there are no data errors.)
Why is it being reset? I've followed the code flow closely and can see the property change event for the combo box's backing field (fwf_Higher_N) being handled, and it appears to somehow come from the line model.ComboSource = repo.GetData();, but the stack only shows [external code] and I don't see why that line would modify an existing object.
The model class
// Names have been changed to protect the innocent
private class MyDataViewModel : INotifyPropertyChanged
{
private DbData _Data;
public DbData Data
{
get { return _Data; }
set
{
_Data = value;
OnPropertyChanged("Data");
}
}
private IQueryable<MyComboModel> _ComboSource;
public IQueryable<MyComboModel> ComboSource {
get { return _ComboSource; }
set
{
_ComboSource = value;
OnPropertyChanged("ComboSource");
}
}
private bool _CanSave;
public bool CanSave
{
get { return _CanSave; }
set
{
_CanSave = value;
OnPropertyChanged("CanSave");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Your description of what is going on and your markup doesn't quite match. I'm going to make some assumptions, such as that Page.DataContext is an instance of MyDataViewModel.
I'm sorry to say it, but a SSCCE would do wonders here. I strongly suggest when anyone gets into situations where they are elbow deep in code they don't quite understand that they break out what they are attempting to do and create a minimal prototype that either exhibits the same behavior, or that helps you learn what's going wrong. I've made 500+ prototypes in the past five years.
As for this situation, you refer to a ComboBox named cboNo in btnSave_Click, but I don't see that in the xaml. This ComboBox's ItemSource appears to be bound to MyDataViewModel.ComboSource.
In addition, all ComboBoxes in the DataGrid also appear to be bound to the model's ComboSource. And, in the button handler event, you change what is in the property:
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
This fires PropertyChanged, and every ComboBox bound to this property will be updated. That means not only cboNo but also every ComboBox in the DataGrid.
It is expected behavior that, when ComboBox.ItemsSource changes, if ComboBox.SelectedItem is not contained within the items source, that SelectedItem is nulled out.
I just spun up a prototype (501+) and it appears that if the IEnumerable that the ComboBox is bound to changes, but the elements in the IEnumerable do not, then SelectedItem is not nulled out.
var temp = combo.ItemsSource.OfType<object>().ToArray();
combo.ItemsSource = temp;
So, within the btnSave_Click event handler, you change this ItemsSource, which probably does not have the same instances that are already in the combo, thus nulling out SelectedItem for all ComboBoxes bound to this property, and then only update cboNo's SelectedIndex.
Now, as for what to do about it...
Well, not sure. From the rest of your code, it appears you need to do some more codebehind work to make sure only the necessary ComboBoxes have their sources updated...

access TextBoxes inside DataTemplate

I know question similar to this has asked several times in SO. But non of them address my issue and have some difficulty with understanding those answers. This is my situation; I have a ItemsControl I have used ItemTemplate and bound some data.
<Window.Resources>
<DataTemplate x:Key="AdditionalFieldTemlate">
<Grid>
<TextBlock Text="{Binding InfoName}"/>
<TextBox Text="{Binding InfoValue,Mode=TwoWay}" Name="CustomValue"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding AdditionalInformation}" x:Name="additionalInfo" ItemTemplate="{DynamicResource AdditionalFieldTemlate}"/>
</Grid>
I need to set TextBox text to empty(all the textboxes text inside datatemplate) once click on a Button. Don't know how to access these textboxes. Please help me.
You don't normally access the TextBoxes (the look) ....you access the data that is being bound to.
So you can just alter the "data" in your collection as follows:
foreach (var item in AdditionalInformation)
{
item.InfoValue = "";
}
The "TextBoxes" will then be emptied.
Make sure you have implemented INotifyPropertyChanged on the class being used by AdditionalInformation....so that when the InfoValue property is altered it raises a notification.
The text in the textboxes is databound to the InfoValue property of your class. Implement the class and proprty like this:
class InfoClass: INotifyPropertyChanged
{
private string _infoValue;
...
public string InfoValue
{
get { return _infoValue; }
set
{
_infoValue = value;
OnNotifyPropertyChanged("InfoValue")
}
}
...
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Then do what colinsmith suggests in your button click handler (or command if you went with the MVVM approach). The binding will be notified to the change and the view will be updated.

WPF DataGrid multiselect binding

I have a datagrid that is multi-select enabled. I need to change the selection in the viewmodel. However, the SelectedItems property is read only and can't be directly bound to a property in the viewmodel. So how do I signal to the view that the selection has changed?
Andy is correct. DataGridRow.IsSelected is a Dependency Property that can be databound to control selection from the ViewModel. The following sample code demonstrates this:
<Window x:Class="DataGridMultiSelectSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<StackPanel>
<tk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" EnableRowVirtualization="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</tk:DataGrid.Columns>
<tk:DataGrid.RowStyle>
<Style TargetType="tk:DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</tk:DataGrid.RowStyle>
</tk:DataGrid>
<Button Content="Select Even" Click="Even_Click" />
<Button Content="Select Odd" Click="Odd_Click" />
</StackPanel>
</Window>
using System.ComponentModel;
using System.Windows;
namespace DataGridMultiSelectSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new[]
{
new MyViewModel {Value = "Able"},
new MyViewModel {Value = "Baker"},
new MyViewModel {Value = "Charlie"},
new MyViewModel {Value = "Dog"},
new MyViewModel {Value = "Fox"},
};
}
private void Even_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[]) DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i%2 == 0;
}
private void Odd_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[])DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i % 2 == 1;
}
}
public class MyViewModel : INotifyPropertyChanged
{
public string Value { get; set; }
private bool mIsSelected;
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (mIsSelected == value) return;
mIsSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Be sure to set EnableRowVirtualisation="False" on the DataGrid element, else there's a risk that the IsSelected bindings fall out of kilter.
I haven't worked with the DataGrid much, but one technique that works for the ListView is to bind to the IsSelected property of the individual ListViewItem. Just set this to true for each object in your list, and then it will get selected.
Maybe the object that represents a row in the DataGrid also has an IsSelected property, and can be used in this way as well?
Guys, thanks for the help. My problem was solved. I think the problem is pretty common for new WPF developers, so I will restate my problem and as well as the solution in more details here just in case someone else runs into the same kind of problems.
The problem: I have a multi-select enabled datagrid of audio files. The grid has multiple column headers. The user can multi-select several row. When he clicks the Play button, the audio files will be played in the order of one the columns headers (say column A). When playback starts, the multi-select is cleared and only the currently playing file is highlighted. When playback is finished for all files, the multi-selection will be re-displayed. The playback is done in the viewmodel. As you can see, there are two problems here: 1) how to select the currently playing file from the viewmodel, and 2) how to signal to the view from the viewmodel that playback is finished and re-display the multi-selection.
The solution: To solve the first problem, I created a property in the viewmodel that is bound to the view's SelectedIndex property to select the currently playing file. To solve the second problem, I created a boolean property in the view model to indicate playback is finished. In the view's code behind, I subscribed the the boolean property's PropertyChanged event. In the event handler, the view's SelectedItems property is re-created from the saved multi-selection (the contents of SelectedItems was saved into a list and SelectedItems was cleared when playback started). At first, I had trouble re-creating SelectedItems. It turned out the problem was due to the fact that re-creation was initiated through a second thread. WPF does not allow that. The solution to this is to use the Dispatcher.Invoke() to let the main thread do the work. This may be a very simple problem for experienced developers, but for newbies, it's a small challenge. Anyway, a lot of help from different people.
Just use SelectedItems on any MultiSelector derived class , and use methods Add, Remove, Clear on IList it returns .

How do I detect row selection in the Xceed DataGrid for WPF

I'm horrible at this WPF thing, so bear with me.
I'm using the Xceed DataGrid for WPF, and I need to know when someone selects a row, but I can't figure out how to do it. I'm sure I need to add some XAML to enable this, but I can't figure out what I should do.
I use a MVVM approach and therefor favor data binding. I will bind the SelectedItem property to a SelectedItem property on my ViewModel object for the grid.
<xcdg:DataGridControl x:Name="grid" SelectedItem="{Binding SelectedItem}">
</xcdg:DataGridControl>
Then on your property setter can do what ever is necessary upon change in the SelectedItemChanged() method.
private IMyItem _selectedItem;
public IMyItem SelectedItem
{
get { return _selectedItem; }
set {
_selectedItem = value;
OnPropertyChanged("SelectedItem");
SelectedItemChanged();
}
}
I'm actually struggling a bit with the same thing myself, except I have a prerequisite that the selection notification be done via an ICommand; however, if you do not have this need, you can wire up the SelectionChanged event handler. It's pretty elementary stuff, but I'll include the code just in case:
XAML:
<Grid>
<DataGrid:DataGridControl x:Name="gridControl" SelectionChanged="gridControl_SelectionChanged">
<!-- Content -->
</DataGrid:DataGridControl>
</Grid>
Code-behind:
private void gridControl_SelectionChanged(object sender, Xceed.Wpf.DataGrid.DataGridSelectionChangedEventArgs e)
{
var selectedIndex = gridControl.SelectedIndex; // int index
var selectedItem = gridControl.SelectedItem; // instance of bound object
var selectedItems = gridControl.SelectedItems; // IList of bound objects
}
All that said, I'm very interested to hear if there are any elegant solutions for getting the selected row from an Xceed DataGrid with an ICommand (in my case, I'm using anonymous types, which can make a difference)...
You don't have to write complicated code for something simple... although it can become tedious, here is some code for you. I hope this helps:
<Style TargetType="xcdg:DataRow">
<EventSetter Handler="dr_PreviewMouseDown" Event="PreviewMouseDown" />
</Style>
void dr_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
DataRow dr = sender as DataRow;
Debug.WriteLine(sender);
}
So here's what I came up with
System.ComponentModel.DependencyPropertyDescriptor gridItemsSourceDescriptor = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(DataGridControl.SelectedItemProperty, typeof(DataGridControl));
gridItemsSourceDescriptor.AddValueChanged(dgBaxRuns, HandleSelectionChanged);
I made for me a easiest way.
<xctk:MaterialButton Margin="5,0,5,0" Grid.Column="3" Content="Szűrt sorok kijelölése" Command="{Binding SelectFilteredRowsCommand}" CommandParameter="{Binding ElementName=MyDataGrid}" />
So, i send my datagrid with my commandparameter to the viewmodel.
public RelayCommand<object> SelectFilteredRowsCommand { get; set; }
SelectFilteredRowsCommand = new RelayCommand<object>((o) =>
{
var datagrid = o as DataGridControl;
if (datagrid != null)
{
var datagriditems = datagrid.Items.Cast<SelectableProduct>();
foreach (SelectableProduct selectableProduct in datagriditems)
{
selectableProduct.IsSelect = true;
}
}
});
And convert back to datagrid itemsoruce type.

Resources