C# WPF MVVM TreeViewItem ToggleButton NEVER shows up - c#

I have recently been working with TreeView to represent files on an SFTP server. The way things load, it gets directories first and adds those to the TreeView. Afterwards, it loops through the directories and and then populates the files.
The Model is as follows.
public class FileData : ViewModelBase
{
private string _Display = "";
public string Display
{
get
{
return _Display;
}
set
{
_Display = value;
OnPropertyChanged(new PropertyChangedEventArgs("Display"));
}
}
}
public class AlbumData : ViewModelBase
{
public AlbumData(string albumName, List<FileData> albumFileList)
{
if (string.IsNullOrEmpty(albumName) || albumFileList == null)
{
return;
}
AlbumName = albumName;
AlbumFiles = albumFileList;
}
public string AlbumName { get; set; } = "";
private List<FileData> _AlbumFiles = new List<FileData>();
public List<FileData> AlbumFiles
{
get
{
return _AlbumFiles;
}
set
{
_AlbumFiles = value;
OnPropertyChanged(new PropertyChangedEventArgs("AlbumFiles"));
}
}
}
Then in the XAML:
<TreeView x:Name="ftpFilesTreeView" ItemsSource="{Binding ServerAlbums}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding AlbumFiles}" DataType="{x:Type local:AlbumData}">
<TextBlock Text="{Binding AlbumName}"/>
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:FileData}">
<TextBlock Text="{Binding Display}"/>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Then I have my ObservableCollection with the data for the TreeView.
private ObservableCollection<AlbumData> _ServerAlbums = new ObservableCollection<AlbumData>();
public ObservableCollection<AlbumData> ServerAlbums
{
get { return _ServerAlbums; }
set
{
_ServerAlbums = value;
OnPropertyChanged(new PropertyChangedEventArgs("ServerAlbums"));
}
}
So, my big issue here is the ToggleButton is not showing up after items are getting added to the TreeView.
I have tried using Blend to create a custom TreeViewItem and tinkering with the HasItems property. I think the issue is HasItems is not getting updated as objects are getting added to to my ServerAlbums[X].AlbumFiles collection.
I was thinking I could do a gross hack and just manually set HasItems when stuff is added, but I feel there needs to be a more elegant solution.

Thanks to a comment from #ASh, I was able to see that I was using a List instead of the correct ObservableCollection.
public class AlbumData : ViewModelBase
{
public AlbumData(string albumName, ObservableCollection<FileData> albumFileList)
{
if (string.IsNullOrEmpty(albumName) || albumFileList == null)
{
return;
}
AlbumName = albumName;
AlbumFiles = albumFileList;
}
public string AlbumName { get; set; } = "";
private ObservableCollection<FileData> _AlbumFiles = new ObservableCollection<FileData>();
public ObservableCollection<FileData> AlbumFiles
{
get
{
return _AlbumFiles;
}
set
{
_AlbumFiles = value;
OnPropertyChanged(new PropertyChangedEventArgs("AlbumFiles"));
}
}
}

Related

Tree View Hierarchical DataTemplate Binding - MVVM

Here I'm trying to bind 'Solution' List to TreeView. Each 'Solution' has 'File' List and 'Solution Name'. I want to use Hierarchical DataTemplate to do this. In Debug mode I checked that 'Solution' List and 'File' list are successfully set. But in my view there is nothing shown.
In addition, in my view class when I try to set Data Type of the Hierarchical DataTemplate, it says "SolutionExplorerModel" does not exist int the namespace even though it does.
ViewModel
public class SolutionExplorerViewModel : INotifyPropertyChanged
{
private List<SolutionExplorerModel> _solutions = new List<SolutionExplorerModel>();
public List<SolutionExplorerModel> Solutions
{
get { return _solutions; }
set
{
_solutions = value;
RaisePropertyChanged("Solutions");
}
}
public SolutionExplorerViewModel()
{
Messenger.Default.Register<OpenFileDialog>(this, OnItemReceived);
}
private void OnItemReceived(OpenFileDialog openFile)
{
var solutionName = openFile.SafeFileName.Replace(".psim", "");
var files = new List<FileModel>();
var solutionPath = openFile.FileName.Replace(openFile.SafeFileName, "");
foreach(var file in Directory.EnumerateFiles(solutionPath, "*.xml"))
{
files.Add(new FileModel(file));
}
var newSolution = new SolutionExplorerModel
{
SolutionName = solutionName,
Files = files
};
_solutions.Add(newSolution);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyThatChanged)
{
//checking if event is not null than raise event and pass
//in propperty name that has changed
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyThatChanged));
}
}
SolutionExplorerModel
public class SolutionExplorerModel : INotifyPropertyChanged
{
private string _solutionName;
public string SolutionName
{
get { return _solutionName; }
set
{
_solutionName = value;
RaisePropertyChanged("SolutionName");
}
}
private List<FileModel> _files;
public List<FileModel> Files
{
get { return _files; }
set
{
_files = value;
RaisePropertyChanged("Files");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyThatChanged)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyThatChanged));
}
}
FileModel
public class FileModel : INotifyPropertyChanged
{
private string _safeName;
public string SafeName
{
get { return _safeName; }
set
{
_safeName = value;
RaisePropertyChanged("SafeName");
}
}
private string _path;
public string Path
{
get { return _path; }
set
{
_path = value;
RaisePropertyChanged("Path");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyThatChanged)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyThatChanged));
}
public FileModel(string path)
{
this.Path = path;
this.SafeName = path.Split('\\').LastOrDefault();
}
}
View
<TreeView ItemsSource="{Binding Solutions}" DataContext="{Binding Source={StaticResource mainViewModelLocater}, Path=SolutionExplorerViewModel}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type model:SolutionExplorerModel}" ItemsSource="{Binding Files}">
<TextBlock Text="{Binding SolutionName}"></TextBlock>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
This should work provided that the SolutionExplorerViewModel property of your mainViewModelLocater actually returns a populated SolutionExplorerViewModel:
<TreeView ItemsSource="{Binding Solutions}" DataContext="{Binding Source={StaticResource mainViewModelLocater}, Path=SolutionExplorerViewModel}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type model:SolutionExplorerModel}" ItemsSource="{Binding Files}">
<TextBlock Text="{Binding SolutionName}"></TextBlock>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type model:FileModel}">
<TextBlock Text="{Binding SafeName}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
Try to set the DataContext explicitly and make sure that you populate the Solutions collection:
treeView.DataContext = new SolutionExplorerViewModel();

Problems with TreeView and Prism-5

I'm having quite a lot of problems trying to build a TreeView using Prism and MVVM. I have my application divided in Regions one of this regions have a module with a TreeView and another region has as module a Ribbon where some kind of projects can be created and configured. Those regions are communicated using prism and that works perfectly. So when a project is created in the "Ribbon Module", the "TreeView Module" gets it and here is where the problem comes.
The ViewModel of the "TreeView Module" has an ObservableCollection where all project should be added. The name of the project and other properties of this class should be showed in the TreeView.
public class Project : BindableBase
{
private List<DataSet> _DataSetList;
private string _projectName;
public Project()
{
DataSetList = new List<DataSet>();
ProjectName = "";
}
public Project(string projectName, List<DataSet> dataSets)
{
ProjectName = projectName;
DataSetList = dataSets;
}
public string ProjectName
{
get { return _projectName; }
set { SetProperty(ref this._projectName, value); }
}
public List<DataSet> DataSetList
{
get { return _DataSetList; }
set { SetProperty(ref this._DataSetList, value); }
}
public bool CheckForLoadedDataSets()
{
foreach (DataSet ds in DataSetList)
{
if(ds.Status != DataSet.DataSetStatus.Loaded)
{
return false;
}
}
return true;
}
}
This is what a DataSet is
public class DataSet : BindableBase
{
public enum DataSetStatus
{
Loaded,
Stopped,
Unloaded,
Empty,
LoadingData,
CorruptData,
};
private string _dataSetSource;
private DataSetStatus _status;
public DataSet(string name, string sourceName, List<Injector> injertors)
{
DataSetName = name;
DataSetSource = sourceName;
Injectors = injertors;
Status = DataSetStatus.Empty;
}
public DataSet(string name, string sourceName)
{
DataSetName = name;
DataSetSource = sourceName;
Injectors = new List<Injector>();
Status = DataSetStatus.Empty;
}
public DataSet(string sourceName)
{
DataSetName = "";
DataSetSource = sourceName;
Injectors = new List<Injector>();
Status = DataSetStatus.Empty;
}
public string DataSetName { get; set; }
public string DataSetSource { get { return _dataSetSource; } set { SetProperty(ref this._dataSetSource, value); } }
public List<Injector> Injectors { get; set; }
public DataSetStatus Status { get { return _status;} set{ SetProperty(ref this._status,value);} }
}
This is the TreeViewViewModel
class TreeProjectManagerViewModel : BindableBase, INavigationAware
{
private ObservableCollection<Project> _projectCollection;
public ObservableCollection<Project> Projects
{
get { return this._projectCollection; }
set
{
SetProperty(ref this._projectCollection, value);
}
}
public TreeProjectManagerViewModel()
{
Projects = new ObservableCollection<Project>();
}
/// <summary>
/// Checking parameters
/// </summary>
/// <param name="navigationContext"></param>
/// <returns></returns>
public bool IsNavigationTarget(NavigationContext navigationContext)
{
if (navigationContext.Parameters["ProjectObject"] != null)
{
return true;
}
return false;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
/// <summary>
/// Getting information from other module
/// </summary>
/// <param name="navigationContext"></param>
public void OnNavigatedTo(NavigationContext navigationContext)
{
Project p = (Project)navigationContext.Parameters["ProjectObject"];
Projects.Add(p);
}
}
And here the code in the View
<Grid>
<TreeView ItemsSource="{Binding Projects}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Projects}">
<TextBlock Text="{Binding Path=ProjectName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
With this I'm able to get the name of the Project in my TreeView but for example, an here my questions:
1.- how can I get a second hierarchy of TreeNodes with the name of each DataSet?
2.- how can I manage the events in my TreeView?
Thank you very much in advance.
Try something like that:
<TreeView ItemsSource="{Binding Projects}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<ie:CallMethodAction MethodName="OnTreeviewLoaded" TargetObject="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:Project}" ItemsSource="{Binding Path=DataSetList}">
<TextBlock Text="{Binding Path=ProjectName}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:DataSet}">
<TextBlock Text="{Binding Path=DataSetName}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
you also need to add :
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ie="http://schemas.microsoft.com/expression/2010/interactions"
into your xaml and create the event handler in your view model

textbox input to get / set viewmodel

I'm sure this has already been asked, but I'm still new to MVVM and WPF, and not too sure what I should be searching for.
I have a viewmodel which includes items in a Model, as well as some additional temporary data items which will all be passed to a process.start(). I have a stackpanel of textbox, and want to allow the user to type in a "ModelName", and if existing, The ViewModel will get and set "TemplateName" associated with the ModelName.
I'm a bit lost on how to implement this. Do I need to create a completely separate ViewModel, which then goes and extracts data from ModelViewModel? Do I just write some code under ModelName's set, where it can validate, query, and set TemplateName?
Model:
public partial class Model
{
public string ModelName { get; set; }
public virtual Template Template { get; set; }
and ViewModel, which takes the Model, and some temporary data:
public class LauncherViewModel:ViewModelBase
{
public LauncherViewModel()
{
_ESTContext = new ESTContext();
Models = new ObservableCollection<Model>(_ESTContext.Models);
}
private ESTContext _ESTContext;
private string _modelname;
private string _serialno;
private string _sonumber;
private string _templatename;
private string _outputname;
private Model _selectedmodel;
public ObservableCollection<Model> Models { get; set; }
public string ModelName
{
get { return _modelname; }
set
{
if (!string.Equals(_modelname, value))
{
_modelname = value;
};
}
}
public string TemplateName { get { return _templatename; }}
public string SerialNo { get { return _serialno; } }
public string SONumber { get { return _sonumber; } }
public string OutputName { get { return _outputname; } }
public Model SelectedModel
{
get { return _selectedmodel; }
set
{
if (_selectedmodel != value)
{
_selectedmodel = value;
}
}
}
}
My View:
<DockPanel>
<StackPanel Margin="0,78,0,68" Width="233" DataContext="{Binding Models}">
<ComboBox IsEditable="True" Text="{Binding ModelName}" SelectedItem="{Binding SelectedModel}"/>
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding SONumber}"/>
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding SerialNumber}"/>
<Button Content="Button"/>
</StackPanel>
</DockPanel>
For your gui to update you must implement INotifyPropertyChanged and a call to it on all your bound properties.
// basic base class for your models, you a
public class ModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator] // remove if you are not using R#
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
// your model
public class Model : ModelBase
{
private string modelName;
private Template template;
public string ModelName
{
get { return modelName; }
set
{
if (value == modelName) return;
modelName = value;
OnPropertyChanged();
}
}
public virtual Template Template
{
get { return template; }
set
{
if (Equals(value, template)) return;
template = value;
OnPropertyChanged();
}
}
}
View:
<DockPanel>
<StackPanel Margin="0,78,0,68" Width="233">
<ComboBox IsEditable="True" Text="{Binding ModelName, Mode='TwoWay'}" SelectedItem="{Binding SelectedModel}" ItemsSource="{Binding Models}"/>
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding SONumber Mode='TwoWay'}"/>
<TextBox Height="23" TextWrapping="Wrap" Text="{Binding SerialNumber Mode='TwoWay'}"/>
<Button Content="Button"/>
</StackPanel>
Note the Mode='TwoWay' this makes the GUI change the values in your viewmodel, rather than just display them. So you need to set that on everything that's supposed to be editable as above. The default behaviour of WPF is that when a control looses focus it will update the bound property, if you set UpdateSourceTrigger='PropertyChanged' the property will be updated each time ie a letter is entered in a text box. I'll leave that part to you, but you will have to do it in your vm! At minimum the properties SONumber,SerialNumber and Models(if it's ref. changes).
VM: I assume you use galasoft here
public class LauncherViewModel : ViewModelBase
{
private ESTContext _ESTContext;
private string _templatename;
private string _modelname;
private string serialNumber;
private string _outputname;
private string modelName;
private ObservableCollection<Model> models;
private Model selectedModel;
private string soNumber;
public LauncherViewModel()
{
// dangerous ;)
_ESTContext = new ESTContext();
Models = new ObservableCollection<Model>(_ESTContext.Models);
}
public ObservableCollection<Model> Models
{
get { return models; }
set
{
if (Equals(value, models)) return;
models = value;
RaisePropertyChanged();
}
}
public string ModelName
{
get { return modelName; }
set
{
if (value == modelName) return;
modelName = value;
RaisePropertyChanged();
}
}
public string TemplateName { get { return _templatename; }}
public string SerialNumber // Note you spelled this wrong in your xaml. SONumber
{
get { return serialNumber; }
set
{
if (value == serialNumber) return;
serialNumber = value;
RaisePropertyChanged();
}
}
public string SONumber
{
get { return soNumber; }
set
{
if (value == soNumber) return;
soNumber = value;
RaisePropertyChanged();
}
}
public string OutputName { get { return _outputname; } }
public Model SelectedModel
{
get { return selectedModel; }
set
{
if (Equals(value, selectedModel)) return;
selectedModel = value;
RaisePropertyChanged();
}
}
}
A cool litle thing if you are using galasoft or sim together with R#.
public class YourViewModelBase : ViewModelBase
{
[NotifyPropertyChangedInvocator] // alt + enter = convert auto property to prop
// with backing field and change notification :)
override protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
base.RaisePropertyChanged(propertyName);
}
// .. :)
}
Cheers,
Stian
If I havent misunderstood your question , Bind Models to ItemsSource of ComboBox instead of Binding it to DataContext of StackPanel
Remove DataContext binding
<StackPanel Margin="0,78,0,68" Width="233" >
Bind Itemssource of ComboBox to Models and you will also have to specify DisplayMemberPath to the property of Model that you want to display in ComboBox.
<ComboBox IsEditable="True" ItemsSource="{Binding Models}" Text="{Binding ModelName}" SelectedItem="{Binding SelectedModel}"/>
And I am assuming you are setting DataContext of window to instance of LauncherViewModel class.
You need to raise the property changed event on properties you bind to i.e.:
private string _ModelName;
public string ModelName
{
get { return _ModelName; }
set
{
if (_ModelName != value)
{
_ModelName = value;
RaisePropertyChanged("ModelName");
}
}
}
In your base view model you need something like this (make sure you implement INotifyPropertyChanged):
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
In the constructor or loaded event of your view add this:
DataContext = new LauncherViewModel();

Add additional control to ObervableCollection

I'm really new to WPF so apologies in adavnced if this is an obvious question. I have a simple Checkbox in XAML as
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid >
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Path=Item.SelectionName}" />
</Grid >
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Simplified code behind to allow bindings and INotifyPropertyChanged is:
public ObservableCollection<CheckedListItem<Selection>> Selections { get; set; }
public class Selection
{
public String SelectionName { get; set; }
}
Selections = new ObservableCollection<CheckedListItem<Selection>>();
Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = "SomeName" }, isChecked: true));
public class CheckedListItem<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private T item;
public CheckedListItem()
{ }
public CheckedListItem(T item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public T Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
}
I now need to add an additional TextBox associated with each Checkbox, so in XAML I have
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" Margin="12,22,12,94">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid >
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Path=Item.SelectionName}" />
<<TextBox />
</Grid >
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I'm a bit stumped how to include this as part of the ObservableCollection and set it up the binding on both the CheckBox and associated TextBox? Both are being added together using Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = "SomeName" }, isChecked: true)); which is causing me some confusion.
EDIT: Added full code
public partial class SelectionSettingWindow : Window
{
public ObservableCollection<CheckedListItem<Selection>> Selections { get; set; }
public class Selection
{
public String SelectionName { get; set; }
public string SelectionTextField { get; set; }
}
public SelectionSettingWindow()
{
InitializeComponent();
Selections = new ObservableCollection<CheckedListItem<Selection>>();
string fg = #"Item1,true,TExtbox1text:Item2,true,TExtbox2text:Item3,false,TExtbox3text"; //Test String
string[] splitSelections = fg.Split(':');
foreach (string item in splitSelections)
{
string[] spSelectionSetting = item.Split(',');
bool bchecked = bool.Parse(spSelectionSetting[1].ToString());
string tbText = spSelectionSetting[2].ToString();
Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = spSelectionSetting[0].ToString(),
SelectionTextField = bText }, isChecked: bchecked));
}
DataContext = this;
}
public class CheckedListItem<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private T item;
private string textField;
public CheckedListItem()
{ }
public CheckedListItem(T item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public T Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public string TextField
{
get { return textField; }
set
{
textField = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("TextField"));
}
}
}
}
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" Margin="12,22,12,94">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}"
Content="{Binding Path=Item.SelectionName}" />
<TextBox Text="{Binding Item.SelectionTextField, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
replace SelectionTextField above with whatever the field is that needs to be edited using the textbox on your Selection class.
Note that I changed the <Grid> to a <StackPanel> So they wouldn't appear on top of eachother and changed the bindings to TwoWay so the changes are reflected in the model.
Make sure your Selection class implements INotifyPropertyChanged (ObservableCollection updates the UI when things get added to/removed from the collection, it doesn't know anything about notifying when it's content's properties change so they need to do that on their own)
Implementing INotifyPropertyChanged on many classes can be cumbersome. I find implementing a base class useful for this. I've got this along with an extra reflection helper for raise property changed available here and a snippet I've made available. It's silverlight but it should work fine for WPF. Using the code I've provided via download you can simply type proprpc and hit tab and visual studio will stub in a property that notifies on change. Some explanation is in one of my old blog posts here and gives credit for where I based the code and snippet from.

ObservableCollection not updating View

I am just starting with MVVM and have hit a hurdle that I hope someone can help me with. I am trying to create a simple View with 2 listboxes. A selection from the first listbox will populate the second list box. I have a class created that stores the information I want to bind to.
MyObject Class (Observable Object is just a base class that implements INotifyPopertyChanged)
public class MyObject : ObservableObject
{
String _name = String.Empty;
ObservableCollection<MyObject> _subcategories;
public ObservableCollection<MyObject> SubCategories
{
get { return _subcategories; }
set
{
_subcategories = value;
RaisePropertyChanged("SubCategories");
}
}
public String Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
public MyObject()
{
_subcategories = new ObservableCollection<EMSMenuItem>();
}
}
In my viewmodel I have two ObservableCollections created
public ObservableCollection<EMSMenuItem> Level1MenuItems { get; set; }
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
In my constructor of the ViewModel I have:
this.Level1MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level2MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level1MenuItems = LoadEMSMenuItems("Sample.Xml");
That works fine for the Level1 items and they correctly show in the View. However I have a command that gets called when the user clicks an item in the listbox, which has the following:
Level2MenuItems = ClickedItem.SubCategories;
For some reason this does not update the UI of the second listbox. If I put a breakpoint at this location I can see that Level2MenuItems has the correct information stored in it. If I write a foreach loop and add them individually to the Level2MenuItems collection then it does display correctly.
Also as a test I added the following to the constructor:
Level2MenuItems = Level1MenuItems[0].SubCategories;
And that updated correctly.
So why would the code work as expected in the constructor, or when looping through, but not when a user clicks on an item in the listbox?
You need to raise the change notification on the Level2MenuItems property.
Instead of having
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
you need
private ObservableCollection<EMSMenuItem> _level2MenuItems;
public ObservableCollection<EMSMenuItem> Level2MenuItems
{
get { return _level2MenuItems; }
set
{
_level2MenuItems = value;
RaisePropertyChanged(nameof(Level2MenuItems));
}
}
The reason the former works in the constructor is that the Binding has not taken place yet. However since you are changing the reference via a command execute which happens after the binding you need to tell view that it changed
You need to make your poco class within the ObservableCollection implement INotifyPropertyChanged.
Example:
<viewModels:LocationsViewModel x:Key="viewModel" />
.
.
.
<ListView
DataContext="{StaticResource viewModel}"
ItemsSource="{Binding Locations}"
IsItemClickEnabled="True"
ItemClick="GroupSection_ItemClick"
ContinuumNavigationTransitionInfo.ExitElementContainer="True">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,10,0" Style="{ThemeResource ListViewItemTextBlockStyle}" />
<TextBlock Text="{Binding Latitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Longitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="5,0,0,0" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class LocationViewModel : BaseViewModel
{
ObservableCollection<Location> _locations = new ObservableCollection<Location>();
public ObservableCollection<Location> Locations
{
get
{
return _locations;
}
set
{
if (_locations != value)
{
_locations = value;
OnNotifyPropertyChanged();
}
}
}
}
public class Location : BaseViewModel
{
int _locationId = 0;
public int LocationId
{
get
{
return _locationId;
}
set
{
if (_locationId != value)
{
_locationId = value;
OnNotifyPropertyChanged();
}
}
}
string _name = null;
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
OnNotifyPropertyChanged();
}
}
}
float _latitude = 0;
public float Latitude
{
get
{
return _latitude;
}
set
{
if (_latitude != value)
{
_latitude = value;
OnNotifyPropertyChanged();
}
}
}
float _longitude = 0;
public float Longitude
{
get
{
return _longitude;
}
set
{
if (_longitude != value)
{
_longitude = value;
OnNotifyPropertyChanged();
}
}
}
}
public class BaseViewModel : INotifyPropertyChanged
{
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnNotifyPropertyChanged([CallerMemberName] string memberName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(memberName));
}
}
}
Your Subcategories property should be read-only.

Categories

Resources