This question is a follow up to this question. I've taken some advice from this site and decided to start learning MVVM implementation for my work with TreeViews. With that being said I am VERY new to MVVM and I'm still getting familiar with the syntax and implementation.
I have a TreeView that displays integer-type data but I would like it to work with strings instead. The tree also allows the user to add to any level by selecting the TreeViewItem and then typing in the new integer header into the textBox, and then clicking a button.
I'd like for a parent, child, and grandchild to be available at start-up with pre-defined names. Another thing to note is that that user will only be able to add to the TreeView at the level of the child (so they'll only be able to add grandchildren).
Model
public class TreeViewModel : PropertyChangedBase
{
public string Value { get; set; }
public ObservableCollection<TreeViewModel> Items { get; set; }
public CollectionView ItemsView { get; set; }
public TreeViewModel(string value)
{
Items = new ObservableCollection<TreeViewModel>();
ItemsView = new ListCollectionView(Items)
{
SortDescriptions =
{
new SortDescription("Value",ListSortDirection.Ascending)
}
};
Value = value;
}
}
ViewModel
public class SortedTreeViewWindowViewModel : PropertyChangedBase
{
private string _newValueString;
public string NewValueString
{
get { return _newValueString; }
set
{
_newValueString = value;
OnPropertyChanged("NewValueString");
}
}
public TreeViewModel SelectedItem { get; set; }
public ObservableCollection<TreeViewModel> Items { get; set; }
public ICollectionView ItemsView { get; set; }
public SortedTreeViewWindowViewModel()
{
Items = new ObservableCollection<TreeViewModel>();
ItemsView = new ListCollectionView(Items) { SortDescriptions = { new SortDescription("Value", ListSortDirection.Ascending) } };
}
public void AddNewItem()
{
ObservableCollection<TreeViewModel> targetcollection;
//Insert the New Node as a Root node if nothing is selected.
targetcollection = SelectedItem == null ? Items : SelectedItem.Items;
if (_newValueString != null)
{
targetcollection.Add(new TreeViewModel(_newValueString));
NewValueString = string.Empty;
}
}
}
View Code-Behind
public partial class Window1 : Window
{
public SortedTreeViewWindowViewModel ViewModel { get { return DataContext as SortedTreeViewWindowViewModel; } set { DataContext = value; } }
public Window1()
{
InitializeComponent();
ViewModel = new SortedTreeViewWindowViewModel()
{
Items = {new TreeViewModel("Test")}
};
}
private void AddNewItem(object sender, RoutedEventArgs e)
{
ViewModel.AddNewItem();
}
private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
ViewModel.SelectedItem = e.NewValue as TreeViewModel;
}
}
Thanks so much for the help. I'm hoping that going through this will help me understand how to edit and build ViewModels so that I can learn to improvise a little more in the future.
UPDATE
The TreeView is now composed of strings, so that part is solved. I still need help with the default nodes though. I have updated my code to reflect this change.
Here are my suggestions
Change the places where you have int to string. The TreeView should handle that change.
In the constructor of your ViewModel, manually insert your default nodes. Make sure you understand how to work with a TreeView as that will affect the design of your Model and ViewModel and naturally improve the implementation.
Here is a very simple example how to fill a tree in the ViewModel which is bound to a TreeView in the View:
View
<Window x:Class="TreeViewExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<StackPanel>
<TreeView ItemsSource="{Binding Tree}"/>
</StackPanel>
</StackPanel>
</Window>
View Code-Behind
namespace TreeViewExample
{
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new MainWindowViewModel();
InitializeComponent();
}
}
}
ViewModel
namespace TreeViewExample
{
using System.Collections.ObjectModel;
using System.Windows.Controls;
class MainWindowViewModel
{
public ObservableCollection<TreeViewItem> Tree { get; set; }
public MainWindowViewModel()
{
Tree = new ObservableCollection<TreeViewItem>();
Tree.Add(GetLoadedTreeRoot());
}
private TreeViewItem GetLoadedTreeRoot()
{
TreeViewItem parent = new TreeViewItem() { Header = "Parent" };
TreeViewItem child1 = new TreeViewItem() { Header = "Child 1" };
TreeViewItem child2 = new TreeViewItem() { Header = "Child 2" };
TreeViewItem grandchild1 = new TreeViewItem() { Header = "Grandchild 1" };
TreeViewItem grandchild2 = new TreeViewItem() { Header = "Grandchild 2" };
child1.Items.Add(grandchild1);
child2.Items.Add(grandchild2);
parent.Items.Add(child1);
parent.Items.Add(child2);
return parent;
}
}
}
Produces:
Parent
Child 1
Grandchild 1
Child 2
Grandchild 2
Additional thoughts:
To clean up your code-behind, you might look up a Command implementation, of which there are many. Although you sometimes need it, avoid code in the code-behind when possible. I really like this example because it shows you a general MVVM implementation without getting into advanced Command-related topics (ItemTemplates, Interactivity namespace, etc.).
Related
I have a main window with a TabControl in it. Each tab contains a UserControl associated with it. In one of my UserControl, I have a button. When I click the button, I would like to change the SelectedIndex of the TabControl that is in my main window.
I'm using the MVVM pattern so if possible, I would like to do it in XAML with the Command property on my button.
For example:
<Button Content="Switch Tab" Command="SwitchTabCommand" />
Thanks in advance my fellow programmers!
EDIT:
The window view model:
public class CoolViewModel : BaseViewModel
{
#region Properties
public ObservableCollection<ITabViewModel> Tabs { get; set; }
public ITabViewModel SelectedTab { get; set; }
#endregion
#region Constructor
public CoolViewModel()
{
Tabs = new ObservableCollection<ITabViewModel>
{
new VeryNiceViewModel(),
new VeryNiceViewModel()
};
}
#endregion
}
Here is the code of a UserControl inside a tab:
public class VeryCoolViewModel : BaseViewModel, ITabViewModel
{
#region Properties
public ObservableCollection<Test> Tests { get; set; }
public Test currentSelection { get; set; }
public string TabHeader { get; set; }
#endregion
#region Commands
ICommand GoToOtherTab { get; set; }
#endregion
#region Constructor
public GabaritSelecteurViewModel()
{
Tests = new ObservableCollection<Test>
{
new Test { Title = "Title #1" },
new Test { Title = "Title #2" },
new Test { Title = "Title #3" },
new Test { Title = "Title #4" },
new Test { Title = "Title #5" }
};
TabHeader = "Tests";
GoToOtherTab = new RelayCommand(GoToTab, parameter => true);
}
#endregion
#region Methods
private void GoToTab(object parameter)
{
// I don't know how to tell to the
// parent window to go to the other tab...
}
#endregion
}
And here's the XAML for the UserControl (that is inside the TabControl):
<Button Content="Go to the other tab" Command="{Binding GoToOtherTab}" />
Give the child viewmodel a public property
ICommand SwitchTabCommand { get {} set { /* INPC stuff */ } }
Bind it to the button's Command property in the usercontrol XAML.
The parent viewmodel can assign a command to the property when it creates the child viewmodel. You can bind a parent vm property to SelectedIndex on the tab control, and the command that the parent creates can set the bound parent viewmodel property.
If you're not going full MVVM and there's no child viewmodel for the usercontrol, make the command property a dependency property of the usercontrol, and bind it to a parent viewmodel command property in the window XAML.
I have a model class that I wish to bind a combo box to. My plan was to have an object with two propertied. 1) an ObservableCollection that contains the items I want to populate the combo box with. 2) A string property that stores the value of the selected item. I cannot seem to get this to work and open to suggestions. I am Trying to follow MVVM as best as possible. The behavior I observe is an empty combo box.
The class looks like this.
public class WellListGroup : Notifier
{
private ObservableCollection<string> _headers;
public ObservableCollection<string> headers
{
get { return this._headers; }
set { this._headers = value; OnPropertyChanged("headers"); }
}
private string _selected;
public string selected
{
get { return this._selected;}
set { this._selected = value; OnPropertyChanged("selected");}
}
}
Notifier looks like:
public class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
And my viewmodel makes a call to a data access layer that creates the following object i wish to bind to.
public class MainViewModel : Notifier
{
public static getWells gw = new getWells();
public static ObservableCollection<string> headers = gw.getHeaders();
public WellListGroup wlg = new WellListGroup {headers = headers, selected = null};
}
Data Access Layer - getHeaders()
public ObservableCollection<string> getHeaders()
{
ObservableCollection<string> vals = new ObservableCollection<string>();
WVWellModel wvm = new WVWellModel();
var properties = getProperties(wvm);
foreach (var p in properties)
{
string name = p.Name;
vals.Add(name);
}
return vals;
}
Then the view:
<ComboBox DockPanel.Dock="Top" ItemsSource = "{Binding Path = wlg.headers}" SelectedItem="{Binding Path = wlg.selected}"></ComboBox>
View Code Behind (Where the Data Context is set)
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
MainViewModel mvm = new MainViewModel();
DataContext = mvm;
}
}
App.xaml.cs
public partial class App : Application
{
private void OnStartup(object sender, StartupEventArgs e)
{
Views.MainView view = new Views.MainView();
view.Show();
}
private void APP_DispatcherUnhandledException(object sender,DispatcherUnhandledExceptionEventArgs e)
{
MessageBox.Show(e.Exception.Message);
e.Handled = true;
}
}
I have tried several iterations of this but cant for the life of me get this to work. I am presented with an empty combo box.
I am going to assume DataContext is set to MainViewModel on the view.
I think you well list group should call OnPropertyChanged
public class MainViewModel : Notifier
{
public static getWells gw = new getWells();
public static ObservableCollection<string> headers = gw.getHeaders();
private WellListGroup _wlg = new WellListGroup {headers = headers, selected = null};
public WellListGroup wlg
{
get { return _wlg; }
set { _wlg = value; OnPropertyChanged("wlg"); }
}
The combo box binding should look like this:
<ComboBox
ItemsSource = "{Binding wlg.headers}"
SelectedItem = "{Binding wlg.selected Mode=TwoWay}"
/>
If neither of those work I would make sure the MainViewModel is being instantiated and assigned to DataContext in the Page constructor or a page loaded event.
Here is a code project Tutorial that may help break down the binding process Step by Step WPF Data Binding with Comboboxes
I create a simple Treeview that I bound to an ObservableCollection.
ObservableCollection<IMarketDataViewModel> MarketDataItems;
public interface IMarketDataViewModel
{
string Title { get; }
ObservableCollection<IMarketDataViewModel> Items { get; set; }
}
public MarketDataUserControl(IMarketDataViewer viewModel)
{
InitializeComponent();
DataContext = viewModel;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
When I update data in my ViewModel, I only see the first level in my Treeview. The only way I found to resolve the problem is to create an event in my ViewModel and when the data is updated instead calling PropertyChange on MarketDataItems, I trigger the event and the View reset marketDataTreeView.ItemsSource like this :
private void ViewModelOnOnUpdateItems()
{
marketDataTreeView.ItemsSource = null;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
And this work perfectly --> All levels are displayed.
Someone know why the PropertyChange doesn't work and why I have to reset the ItemsSource ?
I think you should implement a binding to the ItemSource and this is done by a property:
// Create property
public ObservableCollection<IMarketDataViewModel> MarketDataItems { get; private set; }
...
// Create Binding
Binding bindingObject = new Binding("MarketDataItems");
bindingObject.Source = this; //codebehind class instance which has MarketDataItems
marketDataTreeView.SetBinding(TreeView.ItemsSource, bindingObject);
Or the binding in XAML:
<TreeView x:Name="marketDataTreeView" ItemsSource="{Binding Path=MarketDataItems}"/>
Finally the issue is that I didn't call OnPropertyChanged("Items")
public class MarketDataViewModelBase : IMarketDataViewModel, INotifyPropertyChanged
{
.....
private ObservableCollection<IMarketDataViewModel> items;
public ObservableCollection<IMarketDataViewModel> Items
{
get { return items; }
set
{
items = value;
OnPropertyChanged("Items"); //Add this line fix my issue
}
}
}
I'm familiar with WPF binding and MVVM. One day when answering questions on Baidu Zhidao I encountered such case:
<Grid>
<ListBox Name="lb" DisplayMemberPath="S"/>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.lb.ItemsSource = new ObservableCollection<Item>()
{
new Item("aa"),
new Item("bb"),
new Item("cc"),
};
}
}
public class Item : ListBoxItem //
{
public Item(string s)
{
this.S = s;
}
public string S { get; set; }
}
If Item inherits ListBoxItem or other control, then nothing displays.
But why? Isn't S always a property of an object?
When you bind ItemsSource to data it creates a ListBoxItem for each element and displays it using the DisplayMemberPath you've specified. However if you bind to an array of ListBoxItem that you've created yourself in code-behind then it will use those instead. In the code you posted your Item class inherits ListBoxItem so it's a GUI element, not data. This is poor practice, but if it's what you actually intend to do then you don't need the S property, just set Content directly:
public class Item : ListBoxItem
{
public Item(string s)
{
this.Content = s;
}
}
But if you want to do it properly (e.g. MVVM) then don't inherit ListBoxItem and don't manipulate the UI elements directly in code-behind:
public class Item
{
public Item(string s)
{
this.S = s;
}
public string S { get; set; }
}
DataContextDataContext context1 = new DataContextDataContext();
public MainWindow()
{
InitializeComponent();
DataContext = new ObservableCollection<MyObject>();
RadGridView1.Filtered+=new EventHandler<GridViewFilteredEventArgs>(RadGridView1_Filtered);
ObservableCollection<MyObject> _MyObject = new ObservableCollection<MyObject>();
foreach (var p in context1.Students)
{
_MyObject.Add(new MyObject { ID = p.StudentID, Name = p.StudentFN });
}
}
void RadGridView1_Filtered(object sender, GridViewFilteredEventArgs e)
{
RadGridView1.ItemsSource = ObservableCollection<MyObject>();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
}
}
public class MyObject
{
public int ID { get; set; }
public string Name { get; set; }
}
How do you bind my ObservableCollections to the ItemsSource?
You want to set the ItemSource to the instance of an ObservableCollection you created in the constructor:
RadGridView1.ItemsSource = _MyObject;
You can make the observable collection as a public property in your code-behind/presenter/viewmodel, like
public ObservableCollection<MyObject> MyObjectCollection {get;set;}
then you can populate that and the binding can be code code behind.
ItemsSource is a dependency property you can bind it in XAML or code behind, like suppose you want to bind to ListBox's(say named lstItems) ItemsSource, like (below code is considering that 'MyObjectCollection' is in codebehind
Binding bindingObject = new Binding("MyObjectCollection");
bindingObject.Source = this; //codebehind class instance which has MyObjectCollection
lstItems.SetBinding(ListBox.ItemsSource, bindingObject);
or in XAML,
<ListBox x:Name="lstItems" ItemsSource="{Binding Path=MyObjectCollection}"/>
for both the ways above you need to set the datacontext which is 'this' (for this specific solution).
But maybe you want to look into basic WPF databinding where you can understand Depedency properties, binding objects, binding modes, etc.
http://msdn.microsoft.com/en-us/library/aa480224.aspx
http://msdn.microsoft.com/en-us/library/ms750612.aspx
http://joshsmithonwpf.wordpress.com/2008/05/19/gradual-introduction-to-wpf-data-binding/