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.
Related
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>
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);
}
}
I am using an MVVM pattern and have a ComboBox that binds to properties in the viewmodel like this:
<ComboBox ItemsSource="{Binding Path=ItemCollection}"
SelectedItem="{Binding Path=SelectedItem}">
</ComboBox>
This works fine. In the viewModel I have
private MyData _selectedItem;
public List<MyData> ItemCollection { get; set; }
public MyData SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged();
}
}
which also works fine. The ItemCollection binds to the ItemSource of the ComboBox and the SelectedItem gets updated when a new item is selected in the ComboBox.
I want to manually change the SelectedItem in specific cases. Like this (I skip null checks for the sake of simplicity):
public MyData SelectedItem
{
get { return _selectedItem; }
set
{
if (value.Name == "Jump to the First item")
_selectedItem = ItemCollection.First();
else
_selectedItem = value;
RaisePropertyChanged();
}
}
This assumes that the type MyData has a string property thats called Name.
The problem is that if the conditional statement is true, the ItemSource of the ComboBox WILL get updated, however the actual visible selection of the the comboBox will not.
To give some context the comboBox actually binds to a CompositeCollection where there is one item that is styled as a button, so when clicked a dialog box is opened and the result of the dialogresult is determining what item in the comboBox should be selected..
-- Just no matter what I do the "Button" will always stay selected.
Is your INotifyPropertyChanged interface implemented properly?
Usually you would put your property name e.g. SelectedItem in the call to the function you raise the PropertyChanged event with.
Example courtesy of MSDN below.
https://msdn.microsoft.com/en-us/library/ms743695(v=vs.110).aspx
namespace SDKSample
{
// This class implements INotifyPropertyChanged
// to support one-way and two-way bindings
// (such that the UI element updates when the source
// has been changed dynamically)
public class Person : INotifyPropertyChanged
{
private string name;
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
public Person()
{
}
public Person(string value)
{
this.name = value;
}
public string PersonName
{
get { return name; }
set
{
name = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("PersonName");
}
}
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
}
The problem was that the comboBox got confused when trying to set a selected item while a it was already in the process of setting another selected item.
Solution is to set the IsAsync property to true so the SelectedItem will be set asynchronously.
<ComboBox ItemsSource="{Binding Path=ItemCollection}"
SelectedItem="{Binding Path=SelectedItem, IsAsync=True}">
</ComboBox>
When doing this it is important to invoke code back on the mainthread:
Application.Current.Dispatcher.Invoke(() =>
{
/* code here */
}
});
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.
I have a TreeView where each item has a checkbox. I want a TextBlock to be updated whenever an item is checked or unchecked in the TreeView. The TextBlock's Text should be bound to the CheckedVersions property on my DataContext so that when I read the CheckedVersions property, it gives me a string representing all the checked items in the TreeView. The checked items should be represented in a semicolon-separated string. What would be the best way to do this? I have the following XAML:
<XmlDataProvider Source="XmlData/Versions.xml" XPath="//*[count(*)=0]"
x:Key="versionsXml"
IsInitialLoadEnabled="True" IsAsynchronous="False" />
<HierarchicalDataTemplate x:Key="versionTemplate">
<CheckBox Focusable="False" IsChecked="{Binding Path=IsChecked}"
Content="{Binding Path=Name, Mode=OneTime}"/>
</HierarchicalDataTemplate>
<TreeView x:Name="trv_version"
ItemsSource="{Binding Path=Versions, Mode=OneWay}"
ItemTemplate="{StaticResource versionTemplate}" />
<TextBlock x:Name="txb_version" Text="{Binding Path=CheckedVersions}"
TextWrapping="Wrap" />
Each item in my TreeView is an instance of my VersionViewModel class, which implements INotifyPropertyChanged and notifies when the IsChecked property changes. It seems like I should be able to hook into that so that when IsChecked changes on a VersionViewModel instance in the TreeView, CheckedVersions updates. Maybe if I set UpdateSourceTrigger on the Text binding in the TextBlock? What should I set it to, though?
I think that your tree view model should "know" all the VersionViewModels and then all you need to do is register to the propertychanged event and set the "CheckedVersions" property according to the change.
something like that:
public class treeViewModel : INotifyPropertyChanged
{
public List<VersionViewModel> CurrentVersionViewModel { get; protected set; }
public void AddNewVersionViewModel(VersionViewModel vvm)
{
CurrentVersionViewModel.Add(vvm);
vvm.PropertyChanged += new PropertyChangedEventHandler(
(obj,propEventArgs) =>
{
if (propEventArgs.PropertyName=="IsChecked")
{
// CheckedVersions change logic according to the new value (this is just the concept)
CheckedVersions += (obj as VersionViewModel).IsChecked;
}
}
);
}
public string CheckedVersions { get { return _CheckedVersions; } set { _CheckedVersions = value; RaisePropertyChanged("CheckedVersions"); } }
private string _CheckedVersions;
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string prop)
{
if (PropertyChanged!=null)
{
PropertyChanged(this,new PropertyChangedEventArgs(prop));
}
}
#endregion
}
public class VersionViewModel : INotifyPropertyChanged
{
public bool IsChecked { get; set; }
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}