ObservableCollection Refresh View MVVM - c#

I have an ObservableCollection bound to a ListBox. Selecting an item in the list box populates a user control with it's own viewmodel based on the selected item. I am using a Linq to SQL DataContext for getting data from my model to the viewmodels.
The problem is that the displaymember for the listbox is bound to a property that combines two fields, a number and a date, for the item. The usercontrol allows the user to change the date, and I want that to be reflected in the list box immediately.
I initialize the collection and add in CollectionChanged and PropertyChanged handlers so that the collection is listening for the changes to properties within the collection:
public void FillReports()
{
if (oRpt != null) oRpt.Clear();
_oRpt = new ViewableCollection<Reportinformation>();
//oRpt.CollectionChanged += CollectionChanged; //<--Don't need this
foreach (Reportinformation rpt in _dataDc.Reportinformations.Where(x => x.ProjectID == CurrentPrj.ID).OrderByDescending(x => x.Reportnumber))
{
oRpt.Add(rpt);
}
}
private void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e != null)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged rpt in e.OldItems)
{
rpt.PropertyChanged -= item_PropertyChanged;
}
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged rpt in e.NewItems)
{
rpt.PropertyChanged += item_PropertyChanged;
}
}
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
string s = sender.GetType().ToString();
if(s.Contains("Reportinformation"))
RaisePropertyChangedEvent("oRpt"); //This line does get called when I change the date
else if (s.Contains("Observation"))
{
RaisePropertyChangedEvent("oObs");
RaisePropertyChangedEvent("oObsByDiv");
}
}
The date gets changed correctly and the change persists and is written back to the database, but the change does not reflect in the listbox unless I actually change the collection (which happens when I switch jobs on another control in the same window as the listbox). The line in my property changed handler raises the change event for "oRpt" which is the observable collection bound to the ListBox, and changing the date does call the handler as verified with the debugger:
<ListBox x:Name="lsbReports" ItemsSource="{Binding oRpt}" DisplayMemberPath="ReportLabel" SelectedItem="{Binding CurrentRpt}"
Grid.Row="1" Grid.Column="0" Height="170" VerticalAlignment="Bottom" BorderBrush="{x:Null}" Margin="0,0,5,0"/>
But it seems that simply raising that change doesn't actually trigger the view to refresh the "names" of the items in the listbox. I have also tried to Raise for the ReportLabel bound to the DisplayMemberPath, but that doesn't work (worth a try though). I'm not sure where to go from here, as I think it's bad practice to reload the oRpt collection based on changing the date (therefore the name) of one of the actual items as I expect this database to grow fairly quickly.
Here is the Reportinformation extension class (this is an auto generated LinqToSQL class, so just my part is below):
public partial class Reportinformation // : ViewModelBase <-- take this out INPC already hooked up
{
public ViewableCollection<Person> lNamesPresent { get; set; }
public string ShortDate
{
get
{
DateTime d = (DateTime)Reportdate;
return d.ToShortDateString();
}
set
{
DateTime d = DateTime.Parse(value);
if (d != Reportdate)
{
Reportdate = DateTime.Parse(d.ToShortDateString());
SendPropertyChanged("ShortDate");//This works and uses the LinqToSQL call not my ViewModelBase call
SendPropertyChanged("ReportLabel"); //use the LinqToSQL call
//RaisePropertyChangedEvent("ReportLabel"); //<--This doesn't work
}
}
}
public string ReportLabel
{
get
{
return string.Format("{0} - {1}", Reportnumber, ShortDate);
}
}
public void Refresh()
{
RaisePropertyChangedEvent("oRpt");
}
public string RolledNamesString
{
get
{
if (lNamesPresent == null) return null;
return string.Join("|",lNamesPresent.Where(x=>x.Name!= "Present on Site Walk").Select(x=>x.Name).ToArray());
}
}
}
ANSWER
So my mistake was that I was adding to the LinqToSQL partial classes, and was using my ViewModelBase there which reimplements all of the INPC stuff over top of the autogenerated partial class. I undid that, and just use the INPC from the autogenerated designer stuff and it all works as expected. Thanks to SledgeHammer for chatting and making me rethink all of this!

You can solve this one of two ways. Either your ReportInformation class needs to implement INotifyPropertyChanged and raise the property changed events for the ReportLabel property whenever it changes:
public class ReportInformation : INotifyPropertyChanged
{
private int _numberField;
private DateTime _dateField;
public int NumberField
{
get => _numberField;
set
{
if (_numberField != value)
{
_numberField = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(ReportLabel));
}
}
}
public DateTime DateField
{
get => _dateField;
set
{
if (_dateField != value)
{
_dateField = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(ReportLabel));
}
}
}
public string ReportLabel => $"{NumberField}: {DateField}";
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName]string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
OR, you can use in your ListBox an ItemTemplate rather than DisplayMemberPath like so:
<ListBox x:Name="lsbReports"
ItemsSource="{Binding oRpt}"
SelectedItem="{Binding CurrentRpt}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding NumberField}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding DateField}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Related

UWP Toolkit DataGrid - How to bind CollectionViewSource to ItemsSource of DataGrid?

I'm trying to bind a grouped collection of data items to a DataGrid. The details of the presented data are not relevant, in fact all the contents are set up with dummy data for now.
I followed the sample code found in Microsoft's Sample App and "How to: Group, sort and filter data in the DataGrid Control".
After launching the app the shown DataGrid is empty and the debug output from the binding code says:
Error: Converter failed to convert value of type 'Windows.UI.Xaml.Data.ICollectionView' to type 'IBindableIterable'; BindingExpression: Path='MyContents' DataItem='MyViewModel'; target element is 'Microsoft.Toolkit.Uwp.UI.Controls.DataGrid' (Name='null'); target property is 'ItemsSource' (type 'IBindableIterable').
This is the interesting part of my XAML:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
In my view model I have this code:
public ICollectionView MyContents { get; private set; }
public override void OnNavigatedTo(NavigationEventArgs e)
{
// Irrelevant stuff left out...
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
MyContents = collectionViewSource.View;
}
Is there a conversion from ICollectionView to IBindableIterable? If so, how is it done?
I'm well aware that the examples do the binding in the code, not in the XAML. Does this really make a difference?
If this approach is wrong, how is the correct approach?
Edit:
I'm sorry, I forgot to mention that we use the "MVVM Light Toolkit" by GalaSoft. That's why the code to build the collection is in the view model, not the code behind. And it should stay there.
This has an impact on the kind of binding. To bind to a property of the view model, we use:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
But to bind to a property of the code behind, is has to be:
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents}">
In the meantime, thank you very much to all reading and making suggestions. I'm currently investigating how to connect view model and code behind.
Alright, it took me a 2-digit number of hours to find the root of this problem. There seems to be a disrupted way with Binding compared to x:Bind.
"{Binding} assumes, by default, that you're binding to the DataContext of your markup page." says the documentation "Data binding in depth". And the data context of my page is the view model.
"{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself." says the documentation "{x:Bind} markup extension". Well, and the compile-time generated code has no problems with the different data types.
The XAML is changed to (the Mode is important, because the default is OneTime):
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents, Mode=OneWay}" Loaded="DataGrid_Loaded">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
The code behind needs a property that sends notification events. For this its class needs to inherit from INotifyPropertyChanged. You could use the methods Set() and OnPropertyChanged() shown in #NicoZhu's answer, but this cut-out shows more clearly what is important:
private ICollectionView _myContents;
public ICollectionView MyContents
{
get
{
return _myContents;
}
set
{
if (_myContents != value)
{
_myContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyContents)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if ((sender as DataGrid).DataContext is MyViewModel viewModel)
{
MyContents = viewModel.ContentsView();
}
}
The view model provides the contents view (as a collection of collections) through a method that is called from the code behind. This method is almost identical to the code I used before.
internal ICollectionView ContentsView()
{
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
return collectionViewSource.View;
}
I follow this tutorial creating a simple sample to reproduce your issue, And binding CollectionViewSource works well. Please refer the following code. This is sample project.
Xaml
<controls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlternatingRowBackground="Transparent"
AlternatingRowForeground="Gray"
AreRowDetailsFrozen="False"
AreRowGroupHeadersFrozen="True"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="False"
ColumnHeaderHeight="32"
FrozenColumnCount="0"
GridLinesVisibility="None"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Visible"
IsReadOnly="False"
ItemsSource="{x:Bind GroupView, Mode=TwoWay}"
Loaded="DataGrid_Loaded"
MaxColumnWidth="400"
RowDetailsVisibilityMode="Collapsed"
RowGroupHeaderPropertyNameAlternative="Range"
SelectionMode="Extended"
VerticalScrollBarVisibility="Visible"
>
<controls:DataGrid.RowGroupHeaderStyles>
<Style TargetType="controls:DataGridRowGroupHeader">
<Setter Property="Background" Value="LightGray" />
</Style>
</controls:DataGrid.RowGroupHeaderStyles>
<controls:DataGrid.Columns>
<controls:DataGridTextColumn
Binding="{Binding Name}"
Header="Rank"
Tag="Rank"
/>
<controls:DataGridComboBoxColumn
Binding="{Binding Complete}"
Header="Mountain"
Tag="Mountain"
/>
</controls:DataGrid.Columns>
</controls:DataGrid>
Code Behind
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public ObservableCollection<Item> MyClasses { get; set; } = new ObservableCollection<Item>();
private ICollectionView _groupView;
public ICollectionView GroupView
{
get
{
return _groupView;
}
set
{
Set(ref _groupView, value);
}
}
public MainPage()
{
this.InitializeComponent();
MyClasses.Add(new Item { Name = "Nico", Complete = false });
MyClasses.Add(new Item { Name = "LIU", Complete = true });
MyClasses.Add(new Item { Name = "He", Complete = true });
MyClasses.Add(new Item { Name = "Wei", Complete = false });
MyClasses.Add(new Item { Name = "Dong", Complete = true });
MyClasses.Add(new Item { Name = "Ming", Complete = false });
}
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var groups = from c in MyClasses
group c by c.Complete;
var cvs = new CollectionViewSource();
cvs.Source = groups;
cvs.IsSourceGrouped = true;
var datagrid = sender as DataGrid;
GroupView = cvs.View;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I don't know how transitive WPF C# is to UWP, but this is how I do my observable collection data binding in WPF
In my window's .cs:
public partial class MainWindowView : Window, INotifyPropertyChanged
{
public MainWindowView()
{
InitializeComponent();
this.data.ItemsSource = etc;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Stuff_NThings> etc = new ObservableCollection<Stuff_NThings>();
private void Button_Click(object sender, RoutedEventArgs e)
{
Stuff_NThings t = new Stuff_NThings();
t.stuff = 45;
t.moreStuff = 44;
t.things = 33;
t.moreThings = 89;
etc.Add(t);
}
My class:
public class Stuff_NThings : INotifyPropertyChanged
{
private int _things;
private int _moreThings;
private int _stuff;
private int _moreStuff;
public int things
{
get
{
return _things;
}
set
{
_things = value;
NotifyPropertyChanged(nameof(things));
}
}
public int moreThings
{
get
{
return _moreThings;
}
set
{
_moreThings = value;
NotifyPropertyChanged(nameof(moreThings));
}
}
public int stuff
{
get
{
return _stuff;
}
set
{
_stuff = value;
NotifyPropertyChanged(nameof(stuff));
}
}
public int moreStuff
{
get
{
return _moreStuff;
}
set
{
_moreStuff = value;
NotifyPropertyChanged(nameof(moreStuff));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
By setting the dataGrid's item source in the mainWindow constructor, it will automatically create the headers in the dataGrid based on the class variable names. Whenever you add an instance of Stuff'NThings (via button, other, whatever, and etc) to the observable collection, the trigger is thrown and it updates the UI. Hope some of this actually applies!

WPF combo box does not update currently selected item

I'm having an issue with my combo box. Somehow it can get out of sync with itself. For example, after I change out my BlockSequenceFields, only the dropdown text gets altered. Below, the Field 1 has been updated but you can see that it doesn't reflect in the currently selected item.
My IsSynchronizedWithCurrentItem=true should make the currently selected item behave as expected but it doesn't seem to work. I've read many stackoverflow posts where the current item doesn't match but they just set IsSynchronizedWithCurrentItem to true and it fixes their issue.
Can anyone explain why this isn't working for me?
<ComboBox x:Name="SequenceFieldComboBox"
SelectedItem="{Binding BlockSequenceFieldIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding BlockSequenceFields, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{Binding IsCalibrated, Mode=OneWay}"
IsEnabled="False">
</CheckBox>
<TextBlock
Text="{Binding}">
</TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
EDIT: Further details for Mr. Chamberlain
// ViewModelBase implements INotifyPropertyChanged
public class BlockFieldViewModel : ViewModelBase
{
public BlockSequenceField SequenceField { get; set; }
public List<BlockSequenceCalibrationItemViewModel> Calibrations => this.SequenceField?.CalibrationList;
public bool IsCalibrated => this.Calibrations.TrueForAll(x => x.IsCalibrated == null || x.IsCalibrated == true);
public double AmplitudeThreshold => this.Calibrations.Max(x => x.Amplitude);
public int FieldNumber { get; set; }
public override string ToString()
{
string ret = string.Format(CultureInfo.CurrentCulture, "Field {0} ", this.FieldNumber);
if (Math.Abs(this.AmplitudeThreshold) > .00001)
{
ret = string.Concat(ret, string.Format(CultureInfo.CurrentCulture, "({0} mA)", this.AmplitudeThreshold));
}
return ret;
}
}
And here is the larger viewmodel, call it MainViewModel.cs. Here are the relevant fields in the class
private ObservableCollection<BlockFieldViewModel> blockSequenceFields;
public ObservableCollection<BlockFieldViewModel> BlockSequenceFields
{
get => this.blockSequenceFields;
set
{
this.blockSequenceFields = value;
this.OnPropertyChanged("BlockSequenceFields");
}
}
private void RefreshFieldList()
{
// In order for the combo box text to update, we need to reload the items
var savedIndex = this.BlockSequenceFieldIndex; // to restore to current field.
var fieldList = this.CalibrationViewModel.FieldViewModels;
this.BlockSequenceFields = new ObservableCollection<BlockFieldViewModel>(fieldList);
this.BlockSequenceFieldIndex = savedIndex;
}
Your problem is caused because BlockFieldViewModel does not raise INPC when FieldNumber is updated. You need to raise it for that property at the minimum.
//Assuming the base class looks like
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class BlockFieldViewModel : ViewModelBase
{
//...
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
OnPropertyChanged();
}
}
//...
}
I don't know for sure if this will solve your problem or not, due to the fact that you are using .ToString() to display the name. If you find the above does not fix it trigger a property changed for the entire object by passing a empty string in to your OnPropertyChanged method
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
//Refresh all properties due to the .ToString() not updating.
OnPropertyChanged("");
}
}
Also, if List<BlockSequenceCalibrationItemViewModel> Calibrations can be added to or removed from, or .Amplitude could be changed you need to trigger a refresh of the name from that too.

WPF TreeView CheckBox Binding - How to populate ViewModel with checked boxes

I'm slightly confused about how to set up a CheckBox with a binding that ensures that my ViewModel is populated with all the checked fields. I have provided some of the code and a description at the bottom.
My Xaml file let's call it TreeView.xaml:
<TreeView x:Name="availableColumnsTreeView"
ItemsSource="{Binding Path=TreeFieldData, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate x:Uid="HierarchicalDataTemplate_1" ItemsSource="{Binding Path=Children, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<CheckBox VerticalAlignment="Center" IsChecked="{Binding IsSelected, Mode=TwoWay}">
<TextBlock x:Uid="TextBlock_1" Text="{Binding DisplayName.Text, Mode=OneWay}" />
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The "code behind" TreeView.xaml.cs
public partial class MultipleColumnsSelectorView : UserControl
{
public MultipleColumnsSelectorView()
{
InitializeComponent();
}
private MultipleColumnsSelectorVM Model
{
get { return DataContext as MultipleColumnsSelectorVM; }
}
}
The ViewModel (tried to include only the relevant stuff) MultipleColumnsSelectorVM:
public partial class MultipleColumnsSelectorVM : ViewModel, IMultipleColumnsSelectorVM
{
public ReadOnlyCollection<TreeFieldData> TreeFieldData
{
get { return GetValue(Properties.TreeFieldData); }
set { SetValue(Properties.TreeFieldData, value); }
}
public List<TreeFieldData> SelectedFields
{
get { return GetValue(Properties.SelectedFields); }
set { SetValue(Properties.SelectedFields, value); }
}
private void AddFields()
{
//Logic which loops over SelectedFields and when done calls a delegate which passes
//the result to another class. This works, implementation hidden
}
The model TreeFieldData:
public class TreeFieldData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<TreeFieldData> Children { get; private set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
The Problem:
The behaviour that I want is when the user checks a checkbox, it should set the IsSelected property of TreeField (it does that right now) but then I want to go back to the ViewModel and make sure that this specific TreeField is added to SelectedFields. I don't really understand what the PropertyChangedEvent.Invoke does and who will receive that event? How can I make sure that SelectedFields gets populated so when AddFields() is invoked it has all the TreeField data instances which were checked?
You could iterate through the TreeFieldData objects in the TreeFieldData collection and hook up an event handler to their PropertyChanged event and then add/remove the selected/unselected items from the SelectedFields collection, e.g.:
public MultipleColumnsSelectorVM()
{
Initialize();
//do this after you have populated the TreeFieldData collection
foreach (TreeFieldData data in TreeFieldData)
{
data.PropertyChanged += OnPropertyChanged;
}
}
private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected")
{
TreeFieldData data = sender as TreeFieldData;
if (data.IsSelected && !SelectedFields.Contains(data))
SelectedFields.Add(data);
else if (!data.IsSelected && SelectedFields.Contains(data))
SelectedFields.Remove(data);
}
}
The subscriber of the PropertyChanged event is the view, so that if you change IsSelected programmatically the view knows it needs to update.
To insert the selected TreeField into your list you would add this code to your setter.
Also, you could define the following function which makes the notification much easier if you have many properties:
private void NotifyPropertyChange([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The CallerMemberName attribute instructs the compiler to automatically insert the name of the property calling the method. The ? after PropertyChanged is a shorthand to your comparison to not null.
The setter of IsSelected can then be changed to
set
{
_isSelected = value;
if (value) { viewModel.SelectedFields.Add(this); }
else { viewModel.SelectedFields.Remove(this); }
NotifyPropertyChange();
}
Of course you would need to provide the TreeFieldData with the ViewModel instance, e.g. in the constructor.
I don't know if SelectedFields is bounded/shown in your view. If yes and you want the changes made to the list to be shown, you should change List to ObservableCollection.

Getting IsChecked Property of a CheckBox in a ListBox

So many examples found and none fit! My list box is a list of Result objects. Results can be checked or unchecked in a listbox to mark them as 'Allowed to 'transmit.
<ListBox
x:Name="FileListBox"
ItemsSource="{Binding TestResults}"
ItemTemplate="{StaticResource FileListTemplate}"
SelectionMode="Single"
SelectedItem="{Binding FileListSelected}"
Background="#FFFFFBE2" />
The FileListTemplate
<DataTemplate x:Key="FileListTemplate">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding FileName}" />
<TextBlock Grid.Column="1"
Text="Machine">
</TextBlock>
<CheckBox x:Name="UploadOK"
Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CanUpload, Mode=TwoWay}" />
</Grid>
</DataTemplate>
I took out a lot of formatting code to reduce the clutter. So when the check box is checked (or un checked) I need to set a boolean on the object to true or false. But I do not want the ListItem selected just because the checkbox is selected. When the ListItem is selected something else happens. Here is the code for that.
public TestResult FileListSelected
{
get
{
return selectedItem;
}
set
{
if (value == selectedItem)
return;
selectedItem = value;
if (!Workspaces.Any(p => p.DisplayName == value.FileName))
{
this.DisplayTestResult(value as TestResult);
}
base.RaisePropertyChanged("FileListSelected");
}
}
And here is the code I bound to for the Checkbox (although it didn't work).
public bool CanUpload
{
get { return selectedItem.CanUpload; }
set
{
selectedItem.CanUpload = value;
}
}
I appreciate you looking at this.
Internal Class TestResult
{
...
private bool _canUpload;
public bool CanUpload
{
get { return _canUpload; }
set
{
_canUpload = value;
base.RaisePropertyChanged("CanUpload");
}
}
}
When working with MVVM always check for the following:
Add using System.ComponentModel; to your ViewModelClass
Inherit from INotifyPropertyChanged
Always check your DataContext and see the Output Window for BindingErrors
Create Bindings like this:
Example Property:
public string Example
{
get { return _example; }
set
{
_example= value;
OnPropertyChanged();
}
}
this will call OnPropertyChanged automatically every time a new value is assigned (not updated automaticaly once it changes from some other location!)
Make sure your Implementation of INotifyPropertyChanged looks like this:
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
for that you also need using System.Runtime.CompilerServices;
Other options to get your code working:
Your TestResults sould be an ObservableCollection<TestResult>
TestResult should have a property for CanUpload and FileName and inherit from INotifyPropertyChanged
Then on your MainViewModel for example on and ButtonClick your can get the selected files like this:
private List<string> GetSelectedFiles()
{
return TestResults.Where(result => result.CanUpload == true).Select(r => r.FileName).ToList());
}
Note:
FileListSelected is a Property of your ListBox's DataContext which is different to the DataContext of an entry (or at least should be).
FileListSelected will then return the selected Item of your ItemsSource.
Maybe you can comment on this problem with the row selection/checkbox check and add some detail so I can help you more.
EDIT: Notify MainWindowViewModel about CheckBox State Changes:
I see two possible approaches here:
USING EVENT
Add this to your TestResult class:
public delegate void CheckBoxStateChangedHandler(object sender, CheckBoxStateChangedEventArgs e);
public event CheckBoxStateChangedHandler CheckBoxStateChanged;
public class CheckBoxStateChangedEventArgs
{
bool CheckBoxChecked { get; set; }
}
Make sure that on creation of a new TestResult in your MainViewModel you subscribe to that event;
testResult.CheckBoxStateChanged += CheckBox_StateChanged;
Handle what you want to do once the state is changed in CheckBox_StateChanged. Note that the argument e contains the boolean (Checked) and the corresponding TestResult as the sender.
You simply invoke your new Event in the Setter of your CheckBox.Checked Binding:
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
CheckBoxStateChanged.Invoke(this, new CheckBoxStateChangedEventArgs() { CheckBoxChecked = value })
}
}
CALL METHOD ON MAINWINDOWVIEWMODEL
for that you need o create a static object of your MainWindowViewModel (in your MainViewModel) - don't forget to assigne a value once you create your MainWindowViewModel.
public static MainViewModel Instance { get; set; }
then simply add a public Method as you need:
public void CheckBoxValueChanged(bool value, TestResult result)
{
//Do whatever
}
you can also call in from the same spot as the event from above is invoked.
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
MainWindowViewModel.Instance.CheckBoxValueChanged(value, this);
}
}

Windows 10 Development: How to refresh ListView whenever there is a change in the items inside ListView?

I am very new to the concept of data binding and I don't think I understood it completely. I have a class named Project with a LinkedList of type ToDo as one of its properties. When I navigate to one instance of Project, I will display the LinkedList of type ToDo in a ListView. I have created functions that allow me to change the sequences of the nodes in the LinkedList (move up, move down) and to remove the selected node (delete). I want the ListView to refresh whenever there is a change in the LinkedList, (move up, move down or delete). However, I cannot achieve that. Here is my code: (not all parts are included)
XAML of the page:
<ListView x:Name="myListView" ItemsSource="{Binding Source={StaticResource ToDos}, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox x:Name="myCheckBox"
Content="{Binding ToDoTitle, Mode=TwoWay}"
IsChecked="{Binding IsCompleted, Mode=TwoWay}">
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
C# for DataModel:
public class ToDo : INotifyPropertyChanged
{
private string toDoTitle;
private bool isCompleted;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public string ToDoTitle { get { return this.toDoTitle; } set { this.toDoTitle = value; this.OnPropertyChanged(); } }
public bool IsCompleted { get { return this.isCompleted; } set { this.isCompleted = value; this.OnPropertyChanged(); } }
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
// Raise the PropertyChanged event, passing the name of the property whose value has changed.
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Projects : INotifyPropertyChanged
{
private LinkedList<ToDo> toDos;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public LinkedList<ToDo> ToDos { get { return this.toDos; } set { this.toDos = value; this.OnCollectionChanged(); } }
public Projects()
{
ToDos = new LinkedList<ToDo>();
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
// Raise the PropertyChanged event, passing the name of the property whose value has changed.
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Thank you.
First I would advise you to read about MVVM, and try to follow some basic tutorials like this one.
You can use MVVM Light to avoid managing the INotifyPropertyChanged by yourself at first (but it's really good to know how MVVM light work under the hood).
To come back to your problem, your current code notifies only if you set the full ToDos list. If you want to be aware of any change in a list (seing when an item is add/remove/update), you are probably looking for an ObservableCollection, not a LinkedList.
Hope it helps.

Categories