DataGrid - change edit behaviour - c#

I have an ObservableCollection of ChildViewModels with somewhat complex behaviour.
When I go to edit a row - the DataGrid goes into 'edit-mode' - this effectively disables UI-notifications outside the current cell until the row is committed - is this intended behaviour and more importantly can it be changed?
Example:
public class ViewModel
{
public ViewModel()
{
Childs = new ObservableCollection<ChildViewModel> {new ChildViewModel()};
}
public ObservableCollection<ChildViewModel> Childs { get; private set; }
}
public class ChildViewModel : INotifyPropertyChanged
{
private string _firstProperty;
public string FirstProperty
{
get { return _firstProperty; }
set
{
_firstProperty = value;
_secondProperty = value;
OnPropetyChanged("FirstProperty");
OnPropetyChanged("SecondProperty");
}
}
private string _secondProperty;
public string SecondProperty
{
get { return _secondProperty; }
set
{
_secondProperty = value;
OnPropetyChanged("SecondProperty");
}
}
private void OnPropetyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And in View:
<Window.Resources>
<local:ViewModel x:Key="Data"/>
</Window.Resources>
<DataGrid DataContext="{Binding Source={StaticResource Data}}" ItemsSource="{Binding Childs}"/>
Notice how the second notification when editing first column is hidden until you leave the row.
EDIT: Implementing IEditableObject does nothing:
public class ChildViewModel : INotifyPropertyChanged,IEditableObject
{
...
private ChildViewModel _localCopy;
public void BeginEdit()
{
_localCopy = new ChildViewModel {FirstProperty = FirstProperty, SecondProperty = SecondProperty};
}
public void EndEdit()
{
_localCopy = null;
}
public void CancelEdit()
{
SecondProperty = _localCopy.SecondProperty;
FirstProperty = _localCopy.FirstProperty;
}
}

This behavior is implemented in DataGrid using BindingGroup. The DataGrid sets ItemsControl.ItemBindingGroup in order to apply a BindingGroup to every row. It initializes this in MeasureOverride, so you can override MeasureOverride and clear them out:
public class NoBindingGroupGrid
: DataGrid
{
protected override Size MeasureOverride(Size availableSize)
{
var desiredSize = base.MeasureOverride(availableSize);
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;
}
}
}

This is very old question, but a much better solution which doesn't require subclassing DataGrid exists. Just call CommitEdit() in the CellEditEnding event:
bool manualCommit = false;
private void MyDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (!manualCommit)
{
manualCommit = true;
MyDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
manualCommit = false;
}
}

ok, so, here is the problem. Observable Collection does NOT notify of objects that it contains changing. It only notifies on add/remove/etc. operations that update the collection is-self.
I had this problem and had to manually add my columns to the datagrid, then set the Binding item on the Column object. so that it would bind to my contents.
Also, I made the objects that are in my ICollectionView derive from IEditableObject so when they are "updated" the grid will refresh itself.
this sucks, but its what i had to do to get it to work.
Optionally, you could make your own ObservableCollection that attaches/detaches property changed handlers when an item is addeed and remove.

Related

WPF DataGrid, ObservableCollection Updates, and Redrawing

I will try to keep this as brief as possible, but there is a fair amount of nuance to this question.
The Workflow
I am working in C# and am using WPF and MVVM for the UI for an addin for Revit (a 3D modeling software from Autodesk).
The overarching goal is to create a window that shows the parameters of a 3D element after it is selected. This is a filtered list that are specific to my organization and our users needs, and allow the user to edit them in order to streamline their workflow. The complication is that because I am working with an API I can only use what tools I am given when interacting with the model.
The issue I am running into lies in the workflow. I have detailed the workflow below.
User Selects a 3D Element
The addin uses the API to pull the parameters and wraps them in a wrapper class and adds them into a custom ObservableCollection to display them in the DataGrid
The user then changes a value in the DataGrid. When the cell loses focus it fires off a command that hooks the API and updates the parameter's value.
The change is made and the internal logic of the element calculates it's values based on the changed parameter
The calculated parameter values are changed in the model
The ViewModel checks each parameter to see if its value has changed, and updates any of the wrapped parameters in the ObservableCollection to reflect the changes.
The ObservableCollection fires off it's collection changed event to notify the DataGrid that values have changed
The DataGrid updates it's values to complete the process.
The issue currently lies in the very last step. Once the collection changed event is complete the wrapped parameter value matches the parameter value from the API, but the DataGrid will not redraw the information. Once you minimize the window, click into the cell, or scroll the DataGrid to where the cell is not visible the cell will show the new value when it comes back into view.
I can't seem to find a way to keep with MVVM principles and force the cells to redraw with their updated value. Am I missing something with this? How do I get the DataGrid to update without having to completely clear and reset the ObservableCollection items?
Things I have Tried
I had to create a custom ObservableCollection to implement INPC for the items in the collection, and from debugging it appears to work as intended. Each time an item in the ObservableCollection is updated it makes the change subscribes it to INPC and raises the collection changed event.
For each of the columns I have the binding set to Mode="TwoWay" and have tried setting the UpdateSourceTrigger="PropertyChanged", and neither helped.
I originally was using a <ContentPresenter/> in a <DataGridTemplteColumn/> to present different cell types, but even using a basic <DataGridTextColumn doesn't work.
---- CODE ----
XAML:
<DataGrid Grid.Row="2" ItemsSource="{Binding TestingParameters, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn IsReadOnly="True" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Name"/>
<DataGridTextColumn IsReadOnly="False" Binding="{Binding Path=ParamValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Param Value">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{Binding UpdateParametersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
C# ViewModel
public class ViewModelParameterPane : ViewModelBase
{
private ExternalEvent _event;
private HandlerED _handler;
private UIApplication _uiapp;
private UIDocument _uidoc;
private Document _doc;
private TestObservableCollection<WrappedParameter> _testParameter = new TestObservableCollection<WrappedParameter>();
public TestObservableCollection<WrappedParameter> TestParameter
{
get => _testParameter;
set
{
_testParameter = value;
RaiseProperty(nameof(_testParameter));
}
}
public ViewModelParameterPane(ExternalEvent exEvent, HandlerED handler, UIApplication uiapp)
{
_event = exEvent;
_handler = handler;
_uiapp = uiapp;
_uidoc = _uiapp.ActiveUIDocument;
_doc = _uidoc.Document;
_testParameter.ItemPropertyChanged += _testParameter_ItemPropertyChanged;
_testParameter.CollectionChanged += _testParameter_CollectionChanged;
UpdateParametersCommand = new RelayCommand(CallUpdateParameters);
}
private void _testParameter_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs<WrappedParameter> e)
{
Debug.WriteLine("PROPERTY CHANGE");
RaiseProperty(nameof(TestParameter));
int index = TestParameter.IndexOf(e.Item);
TestParameter[index] = e.Item;
}
private void _testParameter_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Debug.WriteLine("COLLECTION CHANGE");
}
private void MakeRequest(RequestIdED request)
{
_handler.Request.Make(request);
_event.Raise();
}
private void CallUpdateParameters() { MakeRequest(RequestIdED.UpdateParameters); }
public void UpdateParameters()
{
Debug.WriteLine("Running Update Parameters");
try
{
using (var transaction = new Transaction(_doc))
{
transaction.Start("T_UpdateParameters");
foreach (WrappedParameter p in TestParameter)
{
string currenValue = p.RevitParameter.AsValueString();
if (p.RevitParameter.AsValueString() != p.ParamValue)
{
bool setValueSuccess = p.SetRevitParameterValue(p.ParamValue);
if(!setValueSuccess)
{
TaskDialog.Show("Parameter Value Not Set", "The parameter value for the parameter " + p.Name + " was not given a valid value and was not changed. Please ensure the units are correct.");
}
}
}
transaction.Commit();
}
}
catch (Exception e)
{
throw new Exception("Something Went Wrong. Check your values.");
}
}
public void UpdateParameterValues()
{
for(var i = 0; i < TestParameter.Count; i++)
{
TestParameter[i].UpdateValues();
}
}
}
C# Parameter Wrapper Class
public class TestParameter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(_name));
}
}
private string _categories;
public string Categories
{
get => _categories;
set
{
_categories = value;
OnPropertyChanged(nameof(_categories));
}
}
private string _paramValue;
public string ParamValue
{
get => _paramValue;
set
{
_paramValue = value;
OnPropertyChanged(nameof(_paramValue));
}
}
private Parameter _revitParameter;
public Parameter RevitParameter
{
get => _revitParameter;
set
{
_revitParameter = value;
OnPropertyChanged(nameof(_revitParameter));
}
}
private ElementId _elementId;
public ElementId ElementId
{
get => _elementId;
set
{
_elementId = value;
OnPropertyChanged(nameof(_elementId));
}
}
public TestParameter(Parameter param)
{
GetRevitParameterValue();
}
public void GetRevitParameterValue()
{
//Get parameter value logic
}
public bool SetRevitParameterValue(string Value)
{
//Set parameter value logic
}
}
C# TestObservableCollection Class
public class TestObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
item.PropertyChanged -= item_PropertyChanged;
}
protected override void ClearItems()
{
foreach (var item in this)
{
item.PropertyChanged -= item_PropertyChanged;
}
base.ClearItems();
}
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
oldItem.PropertyChanged -= item_PropertyChanged;
base.SetItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e.PropertyName);
}
private void OnItemPropertyChanged(T item, string propertyName)
{
var handler = this.ItemPropertyChanged;
if (handler != null)
{
handler(this, new ItemPropertyChangedEventArgs<T>(item, propertyName));
}
}
}
public sealed class ItemPropertyChangedEventArgs<T> : EventArgs
{
private readonly T _item;
private readonly string _propertyName;
public ItemPropertyChangedEventArgs(T item, string propertyName)
{
_item = item;
_propertyName = propertyName;
}
public T Item
{
get { return _item; }
}
public string PropertyName
{
get { return _propertyName; }
}
}

Access to ObservableCollection

I'm asking how we can acceed to an ObservableCollection.
Actually I'm working on a project and i have to collect the checked elements in an ObservableCollection in order to copy these elements to a PDF file.
public Class FianlElements
{
private int chapAr;
public int ChapAr
{
get { return chapAr; }
set
{
chapAr = value;
OnPropertyChanged("ChaprAr");
OnPropertyChanged("Article");
}
}
private string article;
public string Article
{
get { return article; }
set
{
article = value;
OnPropertyChanged("ChapAr");
OnPropertyChanged("Article");
}
}
private float somme;
public float Somme
{
get { return somme; }
set
{
somme = value;
OnPropertyChanged("Somme");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertySomme)
{
if (PropertyChanged != null)
PropertyChanged(this, new
PropertyChangedEventArgs(propertySomme));
}
}
Actually this is the class of the type of ObservableCollection
My declaration of the ObservableCollection is here
public ObservableCollection<FinalSelection> LesElem { get; set; }
I have another ObservableCollection
public ObservableCollection<ListBoxArticle> LesArticles { get; set; }
This one is Binded to a ListBox which contains CheckBox and TextBox
like this
so I want to copy only the checked elements to "LesElem"
So how can I get access to this ObservableCollection
Thanx
Let's imagine you are interested in LesArticle which are ticked.
AND
You have an IsChecked property on LesArticle.
AND
This is bound to the IsChecked property of the checkbox.
The list of checked ones would be
var checkedList = LesArticle.Where(x => x.IsChecked == true).ToList();
Observablecollection can take a List as it's constructor. So you can do:
FinalElem = new ObservableCollection<ListBoxArticle>(checkedList);

How to properly update UserControl combobox's Itemsource?

I'm completely new to WPF and I'm having problems with ItemsSource updates.
I created a single main window Metro application with tabs (TabItem(s) as UserControl DataContext="{Binding}") in which different data is displayed / different methods used.
What I've found myself struggling with is INotifyPropertyChanged (I wasn't able to understand the solution of my problem from similar examples/questions) interface's concept. I'm trying to make that if new data is entered in a window (which is initialized from one of the UserControl), a ComboBoxin another UserControl (or TabItem) would be automatically updated. Here's what I have:
UserControl1.xaml
public partial class UserControl1: UserControl
{
private userlist addlist;
public UserControl1()
{
InitializeComponent();
fillcombo();
}
public void fillcombo()
{
Fillfromdb F = new Fillfromdb(); // class that simply connects
// to a database sets a datatable as ListCollectionView
addlist = new addlist { List = F.returnlistview() }; // returns ListCollectionView
UsersCombo.ItemsSource = addlist.List;
}
userlist.cs
public class userlist: INotifyPropertyChanged
{
private ListCollectionView _list;
public ListCollectionView List
{
get { return this._list; }
set
{
if (this._list!= value)
{
this._list= value;
this.NotifyPropertyChanged("List");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Registration.xaml (called from another UserControl)
public partial class Registration: MetroWindow
{
public Registration()
{
InitializeComponent();
}
private void confirm_button_click(object sender, RoutedEventArgs e)
{
// new user is saved to database
// * here is where I don't know what to do, how to update the ItemSource
}
}
Here's the ComboBox's setting in UserControl.xaml:
<ComboBox x:Name="UsersCombo"
ItemsSource="{Binding List, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
Since I don't have any programming education/experience a very generic advice/explanation would be very much appreciated.
EDIT: Registration.xaml with propertychanged (still doesn't work):
public partial class Registration : MetroWindow
{
public userlist instance = new userlist();
public ListCollectionView _list1;
public ListCollectionView List1
{
get { return this._list1; }
set
{
if (this._list1 != value)
{
this._list1 = value;
this.NotifyPropertyChanged("List1");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public Registration()
{
InitializeComponent();
instance.List.PropertyChanged += ComboPropertyChangedHandler();
}
private void confirm_button_click(object sender, RoutedEventArgs e)
{
// new user is save to database
// still don't now what to do with new ListCollectionView from database
}
public void ComboPropertyChangedHandler(object obj)
{
List1 = instance.List; // when new data from database should be loaded?
}
This is where PropertyChanged event comes handy.
Bind the combobox in second xaml page to a List and create a similar property like in first xaml.
In second xaml.cs
public partial class Registration: MetroWindow, INotifyPropertyChanged
{
private userlist instance = new userlist();
private ListCollectionView _list1;
public ListCollectionView List1
{
get { return this._list1; }
set
{
if (this._list1 != value)
{
this._list1 = value;
this.NotifyPropertyChanged("List1");
}
}
}
public Registration()
{
InitializeComponent();
instance.List.PropertyChanged += ComboPropertyChangedHandler();
}
private void ComboPropertyChangedHandler(object obj)
{
List1 = instance.List;
//or iterate through the list and add as below
foreach(var item in instance.List)
{
List1.Add(item);
}
}
private void confirm_button_click(object sender, RoutedEventArgs e)
{
// new user is saved to database
// * here is where I don't know what to do, how to update the ItemSource
}
}

Binding and INotifyPropertyChanged

I'm trying to bind a TextBlock using INotifyPropertyChanged event. But it is not updating anything to the TextBlock. The TextBlock is blank. My goal is to update the status of items which are displayed in different rows. I need to update the TextBlock's text and color based on the status.
Could anyone tell me what is wrong with my code?
public class ItemStatus : INotifyPropertyChanged
{
string itemStatus;
Brush itemStatusColor;
public string ItemStatus
{
get { return itemStatus; }
set
{
itemStatus = value;
this.OnPropertyChanged("ItemStatus");
}
}
public Brush ItemStatusColor
{
get { return itemStatusColor; }
set
{
itemStatusColor = value;
this.OnPropertyChanged("ItemStatusColor");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(
this, new PropertyChangedEventArgs(propName));
}
}
public class Items
{
List<ItemStatus> currentItemStatus;
public List<ItemStatus> CurrentItemStatus
{
get { return currentItemStatus; }
set { currentItemStatus = value; }
}
}
public partial class DisplayItemStatus : Page
{
....
....
public DisplayItemStatus()
{
foreach (Product product in lstProductList)
{
TextBlock tbItemStatus = new TextBlock();
....
Items objItems = new Items();
Binding bindingText = new Binding();
bindingText.Source = objItems;
bindingText.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
bindingText.Path = new PropertyPath(String.Format("ItemStatus"));
tbItemStatus.SetBinding(TextBlock.TextProperty, bindingText);
Binding bindingColor = new Binding();
bindingColor.Source = objItems;
bindingColor.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
bindingColor.Path = new PropertyPath(String.Format("ItemStatusColor"));
tbItemStatus.SetBinding(TextBlock.ForegroundProperty, bindingColor);
grdItemsList.Children.Add(tbItemStatus);
}
}
private void UpdateItems_Click(object sender, MouseButtonEventArgs e)
{
int intCount = 0;
List<Product> ProductList = new List<Product>();
List<ItemStatus> ItemList = new List<ItemStatus>();
ProductList = GetProducts();
foreach (Product product in ProductList)
{
intCount++;
UpdateStatus(intCount, ItemList);
}
}
public void UpdateStatus(int intIndex, List<ItemStatus> ItemList)
{
ItemStatus status = new ItemStatus();
status.ItemStatus = strOperationStatus;
status.ItemStatusColor = brshForegroundColor;
ItemList.Add(status);
}
}
Well, the specific problem here is that you're binding the TextBlock to an Item and not the ItemStatus. But you're also doing things the hard way, you really should do the binding details in XAML. Expose a collection of ItemStatus's from your view model, and have a ListBox or something with its ItemsSource bound to the collection. Then you'll need a DataTemplate which defines the TextBlock and the bindings to the ItemStatus.
Here's a good walkthrough for it in general

INotifyPropertyChanged 'Double' binding

I'm trying to bind some XAML code to a property in my ViewModel.
<Grid Visibility="{Binding HasMovies, Converter={StaticResources VisibilityConverter}}">
...
</Grid>
My ViewModel is setup like this:
private bool _hasMovies;
public bool HasMovies
{
get { return _hasMovies; }
set { _hasMovies = value; RaisePropertyChanged("HasMovies"); }
}
In the constructor of the ViewModel, I set the HasMovies link:
MovieListViewModel()
{
HasMovies = CP.Connection.HasMovies;
}
in CP:
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
private ObservableCollection<Movie> _movies;
public ObservableCollection<Movie> MovieList
{
get { return _movies; }
set
{
_movies = value;
RaisePropertyChanged("MovieList");
RaisePropertyChanged("HasMovies");
_movies.CollectionChanged += MovieListChanged;
}
}
private void MovieListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("HasMovies");
}
What am I doing wrong? How should I change this binding so that it reflects the current state of CP.Connection.HasMovies?
Either directly expose the object in the ViewModel and bind directly through that (so that the value is not just copied once which is what happens now) or subscribe to the PropertyChanged event and set HasMovies to the new value every time it changes in your source object.
e.g.
CP.Connection.PropertyChanged += (s,e) =>
{
if (e.PropertyName = "HasMovies") this.HasMovies = CP.Connection.HasMovies;
};
First of all, the setter for a collection type, such as your MovieList property, is not called when you change the content of the collection (ie. Add/Remove items).
This means all your setter code for the MovieList property is pointless.
Secondly, it's very silly code. A much better solution, is to use NotifyPropertyWeaver. Then your code would look like this, in the viewmodel:
[DependsOn("MovieList")]
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
public ObservableCollection<Movie> MovieList
{
get;
private set;
}
Alternatively you would have to add a listener for the CollectionChanged event when you initialize the MovieList property the first time (no reason to have a backing property, really really no reason!), and then call RaisePropertyChanged("HasMovies") in the event handler.
Example:
public class CP : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public CP()
{
MovieList = new ObservableCollection<Movie>();
MovieList.CollectionChanged += MovieListChanged;
}
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
public ObservableCollection<Movie> MovieList
{
get;
private set;
}
private void MovieListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("HasMovies");
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Categories