Refresh a single item in a WPF ItemCollection - c#

I have a DataGrid that is bound to some XML data.
When I make changes in the XML data, the DataGrid does not refresh to reflect those changes.
My "simple" way of fixing this is to call MyDataGrid.Items.Refresh() every time I make a change.
However, this is laggy and seems pretty inefficient.
How can I refresh just a single row, rather than the entire data grid? I have easy access to the DataGridRow as well as the XmlElement that is changed, but I just don't know what function to call.
Been stuck on this problem for 3-4 hours now and have tried dozens of solutions, but just can't get it to work.
Below is relevant code.
A) Defining the style.
<!-- Field Value Style -->
<local:FieldValueConverter x:Key="FieldValueConverter"/>
<local:Node x:Key="Node"/>
<Style x:Key="fieldValueStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Text" Value="{Binding ., Converter={StaticResource FieldValueConverter}}"/>
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
</Style>
B) Defining the DataGrid
<DataGrid x:Name="FieldPanelDataGrid" DockPanel.Dock="Left"
AutoGenerateColumns="False"
DataContext="{Binding ElementName=ObjectPanelListBox, Path=SelectedItem}"
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True"
IsReadOnly="True"
CanUserResizeRows="False"
CanUserResizeColumns="True"
KeyboardNavigation.IsTabStop="False"
Visibility="Visible"
SelectionMode="Single">
<DataGrid.Resources>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="FieldCell_MouseDoubleClick"/>
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="FieldCell_PreviewMouseLeftButonDown"></EventSetter>
<EventSetter Event="PreviewKeyDown" Handler="FieldCell_PreviewKeyDown"></EventSetter>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn x:Name="FieldName" Header="Name" CanUserSort="False" ElementStyle="{StaticResource fieldNameStyle}"/>
<DataGridTextColumn x:Name="FieldValue" Header="Value" Width="*" ElementStyle="{StaticResource fieldValueStyle}"/>
</DataGrid.Columns>
</DataGrid>

I suggest to use an ObservableCollection as ItemSource and the entries in the ObservableCollection have to implement INotifyPropertyChanged. Then you have the benefit if the rows change, the ObservableCollection will tell that your UI and it will update.
Example:
Your entry class:
public class MyXmlObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string fieldName;
public string FieldName
{
get { return fieldName; }
set
{
fieldName = value;
NotifyPropertyChanged("FieldName");
}
}
NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Your code for the UserControl (ViewModel, Controller or Code behind):
public ObservableCollection<MyXmlObject> MyCollection { get; set; }
And as I mentioned in your xaml you simply bind the collection to the ItemsSource
<DataGrid ItemsSource="{Binding MyCollection}" .../>
Now only the items beeing changed get updated.

Related

Cannot get Nested Object to show in DataGrid as Nested Datgrid Row

I am new to WPF and XAML but am picking it up fairly quick I think.
I have a custom project I'm working on that has some objects built out like below. All the data is stored in XML files and loaded into the objects on startup. Note, I'm changing names for sack of privacy on the code/application.
Overarching Class, let's call it Classrooms in a school
Contains a property of Classrooms as ObservableCollection
Uses INotifyPropertyChanged
Primary Object Class, let's call it Classroom. Contains unique properties about the object and a property of Students as ObservableCollection
Uses INotifyPropertyChanged
Secondary Object Class, let's call it. Contains unique properties about the student and a property of Supplies as ObservableCollection
Uses INotifyPropertyChanged
Third Object Class, let's call it SupplyItem. Contains unique properties about the object.
Uses INotifyPropertyChanged
My desire is to have a DataGridView on my MainWindow have a bound instance of the Overarching Collection Class. Each row should represent a Primary Object (Classroom) and show it's properties.
Each Row should have an expander button to expand the row and view a DataGrid with rows for each of the Secondary Object's (Student) associated to the Primary Objects.
The Third object will come later in a separate data grid. I need to get this first part working.
I am struggling to make this work. I have tried so many different ways to do this through XAML I've lost count. Nothing is working.
Intellisense in XAML shows that each property and ItemSource is mapped appropriately. There are no build errors. However, when debugging, I can see eah Primary Object listed in the rows. On expanding Rows that have Secondary Objects, the DataTable Appears, but there is no data listed in the table.
I have also used the Live Property Explorer and Live Visual Tree. I can see all the Primary Objects in the Visual Tree as Rows in the data grid. I can even go into the associated Secondary objects and see the necessary properties for those. I cannot however, get this to display in a working application.
Please help! What am I doing wrong? Greatly appreciate the help.
Overarching Classrooms Class
public class Classrooms : ClassroomBaseClass
{
private static ObservableCollection<Classroom> _colls;
public ObservableCollection<Classroom> Colls
{
get { return _colls; }
set
{
_colls = value;
OnPropertyChanged(nameof(ObservableCollection<Classroom>));
}
}
}
Primary Object Class (Classroom)
public class Classroom : ClassroomBaseClass
{
private Guid? _classroomId;
public Guid? ClassroomId
{
get { return _classroomId; }
set
{
_classroomId = value;
OnPropertyChanged(nameof(ClassroomId));
}
}
private ObservableCollection<Student> _students;
public ObservableCollection<Job> Students
{
get { return _students; }
set
{
_students = value;
OnPropertyChanged(nameof(Students));
}
}
}
Secondary Object Class, Student
public class Student : StudentBaseClass
{
private Guid? _studentId;
public Guid? StudentId
{
get { return _studentId; }
set
{
_studentId = value;
OnPropertyChanged(nameof(StudentId));
}
}
private ObservableCollection<SupplyItem> _supplyitems;
public ObservableCollection<SupplyItem> SupplyItems
{
get { return _supplyitems; }
set
{
_supplyitems = value;
OnPropertyChanged(nameof(SupplyItems));
}
}
}
Third Object Class, SupplyItem
public class SupplyItem : SupplyItemBaseClass
{
private Guid? _supplyId;
public Guid? SupplyId
{
get { return _supplyId; }
set
{
_supplyId = value;
OnPropertyChanged(nameof(SupplyId));
}
}
}
XAML - Top of XAML
xmlns:appObjects="clr-namespace:App.Objects"
<Window.DataContext>
<appObjects:Classrooms/>
</Window.DataContext>
XAML
<DataGrid x:Name="ClassroomDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected"
AutoGenerateColumns="False" RowDetailsVisibilityChanged="ClassroomDataGrid_RowDetailsVisibilityChanged"
Margin="30,60,30,10"
ItemsSource="{Binding Path=Colls}">
<DataGrid.Resources>
<Style BasedOn="{StaticResource {x:Type DataGridColumnHeader}}"
TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="#666666" />
<Setter Property="Foreground" Value="#ffffff" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontStyle" Value="Normal" />
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="GUID" Binding="{Binding Path=ClassroomId,UpdateSourceTrigger=PropertyChanged}"
CanUserResize="True"
Width="200" />
<DataGridTemplateColumn Header="Students Expander" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="appObjects:Classroom">
<Expander Width="50"
Expanded="Expander_Expanded"
Collapsed="Expander_Collapsed"
IsExpanded="{Binding Expanded,UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate >
<DataGrid x:Name="StudentDataGrid"
IsReadOnly="True"
Margin="15,8,8,8"
Width="1000"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Coll.Students}">
<DataGrid.Columns>
<DataGridTextColumn Header="Student Id"
Binding="{Binding Path =StudentId,UpdateSourceTrigger=PropertyChanged}"
Width="Auto"
Visibility ="Hidden"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid.Columns>
</DataGrid>
in the StudentDataGrid ItemsSource should be ItemsSource="{Binding Path=Students}" instead of ItemsSource="{Binding Path=Coll.Students}"
also make sure you have some columns in that DataGrid which are not hidden:
<DataGrid x:Name="StudentDataGrid"
IsReadOnly="True"
Margin="15,8,8,8"
Width="1000"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Students}">
<DataGrid.Columns>
<DataGridTextColumn Header="Student Id"
Binding="{Binding Path=StudentId}"
Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
Thanks to both #ASh and #sfaust for helping me understand this. The answer was in the DataGrid.RowDetailsTemplate / DataTemplate. We had to set the DataType here to the Classroom class as that is what each row represents. Then the DataGrid ItemSource is set to the Classroom ObservableCollection property for Students.
Here's the updated XAML code
'''
<DataGrid.Resources>
<Style BasedOn="{StaticResource {x:Type DataGridColumnHeader}}"
TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="#666666" />
<Setter Property="Foreground" Value="#ffffff" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontStyle" Value="Normal" />
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="GUID" Binding="{Binding Path=ClassroomId,UpdateSourceTrigger=PropertyChanged}"
CanUserResize="True"
Width="200" />
<DataGridTemplateColumn Header="Students Expander" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="appObjects:Classroom">
<Expander Width="50"
Expanded="Expander_Expanded"
Collapsed="Expander_Collapsed"
IsExpanded="{Binding Expanded,UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate DataType="{x:Type appObjects:Classroom}">
<DataGrid x:Name="StudentDataGrid"
IsReadOnly="True"
Margin="15,8,8,8"
Width="1000"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=Students}">
<DataGrid.Columns>
<DataGridTextColumn Header="Student Id"
Binding="{Binding Path =StudentId,UpdateSourceTrigger=PropertyChanged}"
Width="Auto"
Visibility ="Visible"/>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid.Columns>
</DataGrid>
'''

Change ComboBox ItemsSource based on another ComboBox selection

I have two ComboBoxesA and B. First one is populated with a list of items.
I need to change the ItemsSource Binding of the B based on A's SelectedItem
Issue: When X is selected on A, B is not getting populated.
Please note that I'm not filtering the ItemsSource, it a complete change of binding. myItemSources are ObservableCollections
<ComboBox Name="ComboBoxB">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding myItemSource1}" />
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedValue, ElementName=ComboBoxA}" Value="X">
<Setter Property="ItemsSource" Value="{Binding myItemSource2}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
There is an example of how to implement this kind of cascading ComboBoxes using the MVVM design pattern available here: https://blog.magnusmontin.net/2013/06/17/cascading-comboboxes-in-wpf-using-mvvm/
<ComboBox ItemsSource="{Binding Countries}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedCountry}" />
<ComboBox ItemsSource="{Binding Cities}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedCity}"
Margin="0 5 0 0"/>
public CustomObservableCollection<City> Cities {
get;
private set;
}
private Country _selectedCountry;
public Country SelectedCountry {
get {
return _selectedCountry;
}
set {
_selectedCountry = value;
this.Cities.Repopulate(_selectedCountry.Cities);
}
}
...
}

WPF Datagrid cell value edit - C#

I have a simple datagrid with 2 columns that I populate using an observable collection. The requirement is to enable cell edit for one of the columns and then using a button, save the column data somewhere. This is how I have implemented it so far:
View Model:
public class PlanningResult : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public string ProducerName { get; set; }
public string leasename { get; set; }
}
Observable Collection:
ObservableCollection<PlanningResult> populatePatternData = new ObservableCollection<PlanningResult>();
public ObservableCollection<PlanningResult> PopulatePatternData
{
get { return populatePatternData; }
set
{
populatePatternData = value;
base.OnPropertyChanged("StringList");
}
}
XAML : I set the "IsReadOnly=False" for the property ProducerName , hence allowing user to update this value if required.
<DataGrid x:Name="PrintReport" ItemsSource="{Binding PopulatePatternData}" AutoGenerateColumns="False" FontFamily="Tahoma" FontSize="12" CanUserSortColumns="False"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" AlternatingRowBackground="Gainsboro" AlternationCount="1"
SelectionMode="Extended" SelectionUnit="Cell" >
<DataGrid.Columns>
<DataGridTextColumn Header="Pattern" Binding="{Binding ProducerName}" IsReadOnly="False" >
<DataGridTextColumn.ElementStyle>
<Style>
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Lease" Binding="{Binding leasename}" IsReadOnly="True" >
<DataGridTextColumn.ElementStyle>
<Style>
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
My question is what is the next step in terms of how to "get the updated values of the column (ProducerName)" and re-populate the observable collection?
Use a TwoWay mode Binding:
Binding="{Binding ProducerName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
This will update the specific item in the ObservableCollection, whenever the user changes the value of the cell.
Furthermore, use commanding in order to save the current state of your ObservableCollection. See this among many other answer and articles.

DataGridComboBoxColumn doesn't display my collection

I just want to do a simple thing, I just want to show my list of versions into a combobox in a datagrid.
The column "Versions" displays "Collections"... and I don't know why!
Here the code into my xaml:
<DataGrid Name="DataGridTableau" Grid.Column="0" Grid.Row="0"
AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto" CanUserAddRows="True"
ItemsSource="{Binding }"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Sofware" Width="*" IsReadOnly="True" Binding="{Binding Path=Software}">
</DataGridTextColumn>
<DataGridComboBoxColumn Header="Version" Width="*" IsReadOnly="True"
ItemsSource="{Binding Path=Versions}">
</DataGridComboBoxColumn>
</DataGrid.Columns>
</DataGrid>
And into xaml.cs :
public ObservableCollection<ElementTableau> Elements;
public VueEtapeSelection()
{
InitializeComponent();
Elements = new ObservableCollection<ElementTableau>();
List<string> versions = new List<string>();
versions.Add("3.0");
versions.Add("3.1");
Elements.Add(new ElementTableau("Excel", versions));
this.DataGridTableauEKs.DataContext = Elements;
}
public class ElementTableau
{
private string sofware;
public string Software
{
get { return software; }
set { software = value; }
}
private List<string> versions;
public List<string> Versions
{
get { return versions; }
set { versions = value; }
}
public ElementTableau(string software, List<string> versions)
{
this.software = software;
this.versions=versions
}
}
Thanks for your help!
Try setting the ItemsSource using a style:
<DataGrid.Columns>
<DataGridTextColumn Header="Sofware" Width="*" IsReadOnly="True" Binding="{Binding Path=Software}" />
<DataGridComboBoxColumn>
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Path=Versions}" />
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Path=Versions}" />
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
</DataGrid.Columns>
Binding the ItemsSource property of the column doesn't work, because it is evaluated in the context of the column itself which has no parent FrameworkElement. Using the style instead sets the ItemsSource as soon as the ComboBox is being rendered. The ComboBox is integrated in the logical tree and its DataContext can be evaluated, that's why it works.
Edit:
To bind your selected items, add some setters like this:
<Setter Property="SelectedItem" Value="{Binding Path=SelectedVersion}" />

SelectedIndex DataTrigger not working in Combobox

What I want to do is if a combobox has only 1 item then it gets preselected. I tried usind DataTriggers but its not working for 'SelectedIndex' property.But when I set 'IsEnabled' property to false its working and disable the combobox.
Below is my code:
<ComboBox Name="WarehouseCombo"
ItemsSource="{Binding Path=WarehouseList}"
SelectedValue="{Binding Warehouse,Mode=TwoWay}"
Text="{Binding Path=TxtWarehouse,Mode=TwoWay}">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=Items.Count, ElementName=WarehouseCombo}" Value="1">
<Setter Property="SelectedIndex" Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
Please help me as why is this happening in 'SelectedIndex' case.
Don't use the SelectedIndex property. Instead, use the SelectedItem property. One way to fulfil your requirements is to declare a base class for your data collection. Try something like this:
public WarehouseList : ObservableCollection<WarehouseItems>
{
private T currentItem;
public WarehouseList() { }
public T CurrentItem { get; set; } // Implement INotifyPropertyChanged here
public new void Add(T item)
{
base.Add(item);
if (Count == 1) CurrentItem = item;
}
}
Now if you use this class instead of a standard collection, it will automatically set the first item as selected. To make this work in the UI, you simply need to data bind this property to the SelectedItem property like this:
<DataGrid ItemsSource="{Binding WarehouseList}"
SelectedItem="{Binding WarehouseList.CurrentItem}" />

Categories