Binding ICommand WPF ObservableCollection DataGrid - c#

I'm trying to get a WPF MVVM template to work with the basic functionality I am doing in a WPF but non MVVM application. In this case I am trying to capture the RowEditEnding event (which I am) to validate the data on the row that has changed (and this is the problem).
In the XAML I have used an event trigger:
<DataGrid AutoGenerateColumns="False" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch"
ItemsSource="{Binding oDoc.View}">
<DataGrid.Columns>
<DataGridTextColumn x:Name="docIDColumn" Binding="{Binding DocId}" Header="ID" Width="65"/>
<DataGridTextColumn x:Name="DocumentNumberColumn" Binding="{Binding Number}" Header="Document Number" Width="*"/>
<DataGridTextColumn x:Name="altIDColumn" Binding="{Binding AltID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Alt" Width="55"/>
</DataGrid.Columns>
<i:Interaction.Triggers>
<i:EventTrigger EventName="RowEditEnding">
<i:InvokeCommandAction Command="{Binding DocRowEdit}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
With a delegate command to rout to the handler:
public ObservableCollection<Document> oDoc
{
get
{
return _oDoc;
}
}
public ICommand DocRowEdit
{
get { return new DelegateCommand(DocumentRowEditEvent); }
}
public void DocumentRowEditEvent()
{
//How do I find the changed item?
int i = 1;
}
I have not found a way to find the member of the ObservableCollection (oDoc) that has pending changes. I notice that the datagrid is doing some validation, the AltID field that I want to change will highlight red if I put in a non numeric value. But I want to handle the validation, and associated messaging myself. What am I missing? I was thinking to somehow raise a property changed event, but don't find how to wire something like this in:
protected void RaisePropertyChangedEvent(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
The last two code blocks are from my ViewModel class, and I'm trying to do this without any code behind, aside from instantiating the ViewModel in the MainWindow constructor.

You can add a property to your ViewModel:
public Document CurrentDocument
{
get
{
return _currentDocument;
}
set
{
if (value != _currentDocument)
{
_currentDocument = value;
OnPropertyChanged("CurrentDocument") // If you implement INotifyPropertyChanged
}
}
}
and then you can bind it to SelectedItem property of your DataGrid:
<DataGrid AutoGenerateColumns="False" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch"
ItemsSource="{Binding oDoc.View}" SelectedItem="{Binding CurrentDocument}">
Therefore your edit method will be:
public void DocumentRowEditEvent()
{
CurrentDocument.Number = DateTime.Now.Ticks;
/* And so on... */
}
I hope it helps.

Related

WPF MVVM - Get access to DependencyProperty of DataGrid in View from ViewModel

In my View I have a DataGrid which stores objects of 2 descending types. Every row has a Button with a Command connected to the ViewModel. In the ViewModel I need to find out which type of object has been chosen.
The question is what is the best and simple way of accessing SelectedItem property of the DataGrid from the Execute command method in a ViewModel?
So far I did it like this:
var window = Application.Current.Windows.OfType<Window>()
.SingleOrDefault(x => x.IsActive);
var dataGrid = (DataGrid) window.FindName("MyGridName");
...
UPDATE - Xaml:
<DataGrid Name="MyGridName" ItemsSource="{Binding Elements}"
AutoGenerateColumns="False" CanUserAddRows="False"
CanUserDeleteRows="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTemplateColumn Width="auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Name="OptionsBtn" Margin="5" Width="auto"
Height="30" Content="Options"
Command="{Binding ElementName=ElementsViewWindow,
Path=DataContext.ShowOptionsMenuCommand}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
If you take the right MVVM approach this is very easy to do. All you need is to define the item collection of you entities which will be bound to ItemsSource of your DataGrid and a property which will be bound to SelectedItem of your DataGrid. Then in your command you simply reference your selected item property of your model to access the selected item in your DataGrid.
Here is an example implementation with MVVM Light. First you define an observable collection of your entities:
public const string ItemsCollectionPropertyName = "ItemsCollection";
private ObservableCollection<DataItem> _itemsCollection = null;
public ObservableCollection<DataItem> ItemsCollection
{
get
{
return _itemsCollection;
}
set
{
if (_itemsCollection == value)
{
return;
}
_itemsCollection = value;
RaisePropertyChanged(ItemsCollectionPropertyName);
}
}
Then you define a property to hold the selected item:
public const string SelectedItemPropertyName = "SelectedItem";
private DataItem _selectedItem = null;
public DataItem SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem == value)
{
return;
}
_selectedItem = value;
RaisePropertyChanged(SelectedItemPropertyName);
}
}
After that you define a command to handle business logic:
private ICommand _doWhateverCommand;
public ICommand DoWhateverCommand
{
get
{
if (_doWhateverCommand == null)
{
_doWhateverCommand = new RelayCommand(
() => { /* do your stuff with SelectedItem here */ },
() => { return SelectedItem != null; }
);
}
return _doWhateverCommand;
}
}
Finally you create view elements and bind them to the ViewModel:
<DataGrid ItemsSource="{Binding ItemsCollection}" SelectedItem="{Binding SelectedItem}" AutoGenerateColumns="True" />
<Button Content="Do stuff" Command="{Binding DoWhateverCommand}" />
The question is what is the best and simple way of accessing SelectedItem property of DataGrid from Execute command function in a ViewModel?
Just add a property to the view model class where the ShowOptionsMenuCommand property is defined and bind the SelectedItem property of the DataGrid to this one:
<DataGrid Name="MyGridName" ItemsSource="{Binding Elements}" SelectedItem="{Binding SelectedElement}" ... >
Then you can access the source property (SelectedElement or whatever you choose to call it) directly from the Execute method.
The other option would be to pass the item as a CommandParameter to the command:
<Button Name="OptionsBtn" ... Content="Options"
Command="{Binding ElementName=ElementsViewWindow, Path=DataContext.ShowOptionsMenuCommand}"
CommandParameter="{Binding}" />

Is there a way to force entry of a WPF DataGrid column before other columns?

I have an editable DataGrid with a column A and B. When adding a new item/row, it doesn't make sense for the user to enter anything in column B until they enter something in column A. Is there a way to force the user, when creating a new item, to initialize column A first? I'm able to use RowValidationRules just fine to ensure column A is initialized, but I'm trying to find a way to prevent that altogether.
I've also looked at handling Begin/Prepare cell edit events and cancelling edit of column B until A is initialized. The problem there is that the user can enter a value for A but it is not yet visible/pending, and has not yet been committed by the DataGrid.
Assuming that this is your model
public class Model : INotifyPropertyChanged
{
string _desc;
public string Desc { get { return _desc; } set { _desc = value; RaisePropertyChanged("Desc"); } }
string _name;
public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
and first column is bound to Name and second column to Desc:
<DataGrid.Columns>
<DataGridTextColumn Header="A" Binding="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
<DataGridTextColumn Header="B" Binding="{Binding Desc}"/>
</DataGrid.Columns>
you can handle BeginningEdit event, as you said. Note that in Binding the first column I set UpdateSourceTrigger=PropertyChanged. This solve the issue you pointed out:
it is not yet visible/pending, and has not yet been committed by the DataGrid
Now you can check the requirements and Cancel them if needed. For example:
private void dg_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (((DataGrid)sender).CurrentCell.Column.DisplayIndex == 1 &&
((Model)e.Row.Item).Name == null)
{
MessageBox.Show("Insert value of the first column first!");
e.Cancel = true;
}
}
You could do this using a DataGridTemplateColumn for Column B.
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ColPropertyB}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=ColPropertyB}"
IsReadOnly="{Binding Path=ColPropertyA, Converter={StaticResource IsPropertyInvalidConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
Instead of using the IsReadOnly property you could also use Visibility and multiple UIElements or what ever you want.
A second solution could be the usage of a CellEditingTemplateSelector and two CellEdititingTempates. The first with TextBlock if ColA is invalid and a TextBox otherwise. The TemplateSelector validates property A and returns the correct Template.
<!-- DataGrid ResourceDictionary -->
<DataTemplate x:Key="ValidTemplate">
<TextBox Text="{Binding Path=ColPropertyB}" />
</DataTemplate>
<DataTemplate x:Key="InvalidTemplate">
<TextBlock Text="{Binding Path=ColPropertyB}" />
</DataTemplate>
<ColBTemplateSelector x:Key="ColBTemplateSelector"
ValidDataTemplate="{StaticResource ValidTemplate}"
InvalidDataTemplate="{StaticResource InvalidTemplate}" />
<!-- DataGridColumns -->
<DataGridTemplateColumn CellEditingTemplateSelector="{StaticResource ColBTemplateSelector}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=RowPropertyB}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
TemplateSelector
public class ColBTemplateSelector : DataTemplateSelector
{
public DataTemplate ValidDataTemplate { get; set; }
public DataTemplate InvalidDataTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var tempItem = (item as YourDataItem);
if (tempItem == null || tempItem.IsColPropertyAInvalid()) return InvalidDataTemplate;
return ValidDataTemplate;
}
}
With the TemplateSelector I'm not sure if it is called always when entering editing mode...

Listview Binding Does Not Update Programmatic Changes

I've done some research on the topic, and while I've come across some possibilities, nothing has worked for me.
Details:
I'm working on a WPF app using an MVVM design pattern. In the ViewModel, I have a List of Notes, a class with a few properties (among them, Note). I've created a property, SelectedNote on the VM to hold the currently selected note.
In my View, I've bound a ListView control to the list QcNotes. I've bound a TextBox to the SelectedNote property. When I make changes to the TextBox, they are correctly reflected in the appropriate row of the ListView.
Problem:
I've include a RevertChanges command. This is a relatively simple command that undoes changes I've made to the note. It correctly updates the TextBox, and it actually updates the underlying list correctly, but the changes do not update the ListView itself. (Is it necessary to use an ObservableCollection in this circumstance? I've been asked to try and resolve the problem without doing so).
Attempted Fixes
I tried to call NotifyPropertyChanged("SelectedNote") and NotifyPropertyChanged("QcNotes") directly from within the call to RevertChanges, but that hasn't fixed the problem.
Any ideas?
XAML
<Window.DataContext>
<VM:MainProjectViewModel />
</Window.DataContext>
<Grid>
<StackPanel>
<ListView ItemsSource="{Binding QcNotes, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" x:Name="list" SelectedItem="{Binding SelectedNote}">
<ListView.View>
<GridView>
<GridViewColumn Header="Note" DisplayMemberBinding="{Binding Note}" />
</GridView>
</ListView.View>
</ListView>
<TextBox
Height="30"
HorizontalAlignment="Stretch"
Text="{Binding SelectedNote.Note, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Allow Edits" Command="{Binding ChangeStateToAllowEditsCommand}" />
<Button Content="Save Changes" Command="{Binding EditNoteCommand}" />
<Button Content="Revert Changes" Command="{Binding RevertChangesToNoteCommand}" />
</StackPanel>
</StackPanel>
</Grid>
ViewModel Code
public class MainViewModel : BaseViewModel
{
private QcNote selectedNote;
private string oldNoteForUpdating;
private VMState currentState;
private string noteInput;
private IList<QcNote> qcNotes;
public IList<QcNote> QcNotes
{
get
{
return qcNotes;
}
set
{
qcNotes = value;
NotifyPropertChanged();
}
}
public QcNote SelectedNote
{
get
{
return selectedNote;
}
set
{
selectedNote = value;
oldNoteForUpdating = SelectedNote.Note;
NotifyPropertChanged();
}
}
public VMState CurrentState
{
get
{
return currentState;
}
set
{
currentState = value;
NotifyPropertChanged();
}
}
public ICommand RevertChangesToNoteCommand
{
get
{
return new ActionCommand(o => RevertChangestoNote());
}
}
private void RevertChangestoNote()
{
QcNotes.First(q => q.Id == SelectedNote.Id).Note = oldNoteForUpdating;
SelectedNote.Note = oldNoteForUpdating;
NotifyPropertChanged("SelectedNote");
NotifyPropertChanged("QcNotes");
CurrentState = VMState.View;
}
I'll post an answer to my own question, but don't want to deter other from offering suggestions.
I implemented the INotifyPropertyChanged interface on my Models.QcNote class, and that resolved the issue. Initially, the interface was implemented exclusively on the ViewModel. In that case, NotifyPropertyChanged was only called when the QcNote object itself was changed, not when the properties of the object were changed.

Find Textbox in DataGrid WPF

I edited last question.
I have 2 classes
public class Signal: INotifyPropertyChanged
{
public string Name { get; set;}
public Int32 Value { get; set;}
private ObservableCollection < RawVal > rawValue1;
public ObservableCollection < RawVal > rawValue
{
get { return rawValue1; }
set
{ rawValue1 = value;
OnPropertyChanged("rawValue");
if (value != null && value.Count > 0)
{
SelectedRaValue = value.First();
}
}
}
private RawVal selectedRaValue;
public RawVal SelectedRaValue
{
get
{
return selectedRaValue;
}
set
{
selectedRaValue = value;
OnPropertyChanged("SelectedRaValue");
ComboValue = value.name;
OnPropertyChanged("ComboValue");
}
}
public string ComboValue
{
get;
set;
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
And Here XAML:
<DataGrid ItemsSource="{Binding}" Name="grdSignal" Grid.Row="1" CanUserAddRows="False" AutoGenerateColumns="False" SelectionChanged="grdSignal_SelectionChanged_1">
<DataGrid.Columns>
<DataGridTextColumn Header="Signal Name" Binding="{Binding Name}" Width="150"/>
<DataGridTemplateColumn Header="Physical Value" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding rawValue, Mode=TwoWay}" SelectedItem="{Binding SelectedRaValue,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="name" Name="cmbVal"
IsEditable="True" KeyDown="cmbVal_KeyDown" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Value" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding ComboValue}" Name="tBoxValue" TextChanged="tBoxVale_textChanged"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Comment" Width="200">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Comment}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Now, here comes my problem. Combobox is Editable, if user enters anything (string), and presses ENTER Key, Combobox text goes to Textbox Text.
private void cmbVal_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)
{
string s = ((ComboBox)sender).Text;
DataGridRow row = sigGrid.ItemContainerGenerator.ContainerFromIndex(sigGrid.SelectedIndex) as DataGridRow;
var i = 3;
TextBox ele = sigGrid.FindName("tbValue") as TextBox;
}
}
And Result is:
#Mamurbek,
You could use the FindVisualDescendants function of that post :
Datagrid templatecolumn update source trigger explicit only updates first row
Using that function, you could have all textboxes for instance :
var textboxes = AccessGrid.FindAllVisualDescendants()
.Where(elt => elt.Name == "tBoxValue" )
.OfType<TextBox>();
Only the first for instance :
var textboxes = AccessGrid.FindAllVisualDescendants()
.Where(elt => elt.Name == "tBoxValue" )
.OfType<TextBox>()
.FirstOrDefault();
Or only the 3rd using Skip and Take :
int n = 3;
var textboxes = AccessGrid.FindAllVisualDescendants()
.Where(elt => elt.Name == "tBoxValue" )
.OfType<TextBox>()
.Skip(n-1)
.Take(1);
Hope it helps
If you use bindings in your xaml, you obviously have a object used as a datacontext for your grid named grdSignal.
And from this line...
<TextBox Text="{Binding Comment}"/>
I can see that the bound objects have a Comment property:
If you want to read or write this value, you have to locate the respective property is your data source which is bound to the grid (used a data context) and read or assign this property.
In your case, if your assign a value to this property (if it implements INotifyPropertyChanged) the TextBox will update automatically to the new value.
In summary:
If you use bindings, you have to read and assign from / to properties of
your data classes in order to communicate with the UI. Bindings connect targets (controls in your UI) to sources (data in your data context class or view model), so there should be no need to name controls in your view.
If you do not use bindings, you can communicate with the UI using the code behind file and accessing elements by their names, but this is not the way it should be done in WPF (not MVVM style)

WPF DataGrid Get Row Item

i am new to wpf, i have datagrid as follows,
<DataGrid Grid.Row="0" x:Name="dg1" Grid.Column="0" SelectionChanged="DataGrid_SelectionChanged" ItemsSource="{Binding Path=Articles}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Description" Binding="{Binding Path=Description}" />
</DataGrid.Columns>
</DataGrid>
ViewModel has property
public IEnumerable<Article> Articles
{
get
{
return _ArticleList;
}
}
I am not able to get the selected item, following code returns error.
Unable to cast object of type 'MS.Internal.NamedObject' to type 'Article'.
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Article Article = (Article)this.dg1.SelectedItems;
}
Please any suggestions how to implement the same??
EDIT:
DataGrid.SelectedItems is an IList. if you want just the Selected One use DataGrid.SelectedItem <-- without s :)
if you want to set the SelectedItem via Binding:
public Article SelectedArticle
{
set
{
this._selectedArticle= value;
OnPropertyChanged("SelectedArticle");
}
get
{
return _selectedArticle;
}
}
XAml
<DataGrid SelectedItem="{Binding SelectedArticle, Mode=OneWayToSource}" />
or as CommandParameter for Button with ICommand
<Button Command="{Binding EditDataCommand}" CommandParameter="{Binding ElementName=MyGridCtrl, Path=MyDataGrid.SelectedItem}"/>
or all SelectedItems for Button with ICommand
<Button Command="{Binding DeleteDataCommand}" CommandParameter="{Binding ElementName=MyGridCtrl, Path=MyDataGrid.SelectedItems}" >
You need to check if items are selected:
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(this.dg1.SelectedItems.Count > 0){
Article article = (Article)this.dg1.SelectedItems;
}
}
Hope this works...
Article _article =
((dgReferences.SelectedItem as
DataGridRow).Item as Article);
Regards
ArunDhaJ

Categories