I'm trying to build a TreeView using MVVM in a WPF App but I don't understand how to handle HierarchicalDataTemplate. My TreeView should represent a folder structure which contains folders within folders and so on.
My folder ViewModel is defined as follows:
public class TreeViewFolderViewModel : ViewModelBase
{
private int _id;
private int _parentId;
private string _text;
private string _key;
private ObservableCollection<TreeViewFolderViewModel> _children;
public int Id
{
get { return this._id; }
set { Set(() => Id, ref this._id, value); }
}
public int ParentId
{
get { return this._parentId; }
set { Set(() => ParentId, ref this._parentId, value); }
}
public string Text
{
get { return this._text; }
set { Set(() => Text, ref this._text, value); }
}
public string Key
{
get { return this._key; }
set { Set(() => Key, ref this._key, value); }
}
public ObservableCollection<TreeViewFolderViewModel> Children
{
get { return this._children ?? (this._children =
new ObservableCollection<TreeViewFolderViewModel>()); }
set { Set(() => Children, ref this._children, value); }
}
}
My model has the same structure as my ViewModel so the final ViewModel is a list of folders that contain child folders and so forth. I'm using recursion to load all these folders and that part is working fine.
Where I'm stuck is on how to define and load this ViewModel into the actual TreeView.
I've read Hierarchical DataBinding in TreeView using MVVM pattern and while I more or less understand what's going on, but each of the levels of the TreeView represent a different object type while my TreeView has only one object type and I'm confused as to how I'm suppose to define this.
The Root ViewModel property in my MainWindowViewModel is of type TreeViewFolderViewModel which means I have a single object which represents to root of my TreeView. This object has Children of type TreeViewFolderViewModel which in turn have also Children of type TreeViewFolderViewModel and so forth
How do I defined this in XAML? I have the following defined:
<TreeView Grid.Row="1" Margin="5,0,5,5" ItemsSource="{Binding RootFolder}"/>
And I've got a Hierarchical template defined as follows:
<Window.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"
DataType="{x:Type viewmodels:SharePointFolderTreeViewViewModel}">
<Label Content="{Binding Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
But nothing is loading up.
Any ideas on how I can resolve this?
Thanks.
I prepared a small sample to illustrate.
ViewModels
public class TreeViewFolderViewModel : ViewModelBase
{
private int id;
public int Id
{
get { return id; }
set { id = value; OnPropertyChanged("Id"); }
}
private string text;
public string Text
{
get { return text; }
set { text = value; OnPropertyChanged("Text"); }
}
private ObservableCollection<TreeViewFolderViewModel> children;
public ObservableCollection<TreeViewFolderViewModel> Children
{
get
{
return children ?? (children =
new ObservableCollection<TreeViewFolderViewModel>());
}
set { children = value; OnPropertyChanged("Children"); }
}
}
public class TreeViewModel : ViewModelBase
{
private List<TreeViewFolderViewModel> items;
public List<TreeViewFolderViewModel> Items
{
get { return items; }
set { items = value; OnPropertyChanged("Items"); }
}
public TreeViewModel()
{
Items = new List<TreeViewFolderViewModel>()
{
new TreeViewFolderViewModel()
{
Id =0, Text="RootFolder", Children=new ObservableCollection<TreeViewFolderViewModel>()
{
new TreeViewFolderViewModel() { Id = 10, Text = "FirstFolder", Children=new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 11, Text = "FirstChild" } } } ,
new TreeViewFolderViewModel() { Id = 20, Text = "SecondFolder", Children = new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 21, Text = "SecondChild" } } } ,
new TreeViewFolderViewModel() { Id = 30, Text = "ThirdFolder", Children = new ObservableCollection<TreeViewFolderViewModel>() { new TreeViewFolderViewModel() { Id = 31, Text = "ThirdChild" } } }
}
}
};
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TreeViewModel />
</Window.DataContext>
<Window.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"
DataType="{x:Type local:TreeViewFolderViewModel}">
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="Id" />
<Binding Path="Text" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Items}" />
</Grid>
</Window>
Related
In my view, I have a ListBox with some templated items that contain buttons.
<ListBox x:Name="MyListBox" ItemTemplate="{DynamicResource DataTemplate1}"
ItemsSource="{Binding MyItems}">
</ListBox>
And the template for generated items:
<DataTemplate x:Key="DataTemplate1">
<StackPanel Orientation="Horizontal">
<Button Width="50" Click="Button_Click" />
</StackPanel>
</DataTemplate>
When user clicks a button on one of those ListBox items, I want to send the index of that ListBox item to my ViewModel.
So figured to use Binding as it seems to be the way in MVVM. But I'm struggling to set up a binding in code between two properties.
My View code is as follows:
public partial class ItemView : UserControl
{
ViewModel.ItemViewModel VM;
public ItemView()
{
InitializeComponent();
VM = new ViewModel.ItemViewModel();
this.DataContext = VM;
}
private int clickedItemIndex;
public int ClickedItemIndex { get => clickedItemIndex; set => clickedItemIndex = value; }
private void Button_Click(object sender, RoutedEventArgs e)
{
var ClickedItem = (sender as FrameworkElement).DataContext;
ClickedItemIndex = MyListBox.Items.IndexOf(ClickedItem);
}
}
I get the index and set it to ClickedItemIndex property,
I also have property in my ViewModel:
public int SomeInt { get; set; }
Now how do I set up a binding between these two properties?
I'm quite new to MVVM and still learning it. So, maybe this not the correct approach. But I need to have a way for each individual listbox item to be able to call upon an effect in more global viewmodel. For example, if I wanted to have a "Remove" button on each of the listbox items, I would somehow need to send the index to the viewmodel and call the removeItem method with index as the parameter. Or is there a better way to do similar things?
I have a sample app created just for this scenario. I know it seems a lot of code at first glance. Copy this code in your project, that will help debug and get a hang of it(MVVM, databinding, commands and so on).
usercontrol.xaml
<UserControl x:Class="WpfApplication1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type local:Model}">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Path=Name}"/>
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=DataContext.UpdateCommand}"
CommandParameter="{Binding}"
Content="Update"/>
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl},Path=DataContext.RemoveCommand}"
CommandParameter="{Binding}"
Content="Remove"/>
</StackPanel>
</DataTemplate>
</Grid.Resources>
<ListBox ItemsSource="{Binding Models}">
</ListBox>
</Grid>
usercontrol.cs
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
View model
public class ViewModel : INotifyPropertyChanged
{
private Models _Models;
public Models Models
{
get { return _Models; }
set { _Models = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Models)));
}
}
public ViewModel()
{
Models = new Models();
UpdateCommand = new Command(o => true, UpdateItem);
RemoveCommand = new Command(o => true, RemoveItem);
}
void RemoveItem(object item)
{
Model m = (item as Model);
Models.Remove(m);
}
void UpdateItem(object item)
{
Model m = (item as Model);
m.Name = m.Name + " updated";
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public ICommand UpdateCommand { get; private set; }
public ICommand RemoveCommand { get; private set; }
}
Icommand implementation
public class Command : ICommand
{
private readonly Func<object, bool> _canExe;
private readonly Action<object> _exe;
public Command(Func<object,bool> canExecute,Action<object> execute)
{
_canExe = canExecute;
_exe = execute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExe(parameter);
}
public void Execute(object parameter)
{
_exe(parameter);
}
}
Model and a collection of models
public class Models : ObservableCollection<Model>
{
public Models()
{
Add(new Model ());
Add(new Model ());
Add(new Model ());
Add(new Model ());
}
}
public class Model : INotifyPropertyChanged
{
static int count = 0;
public Model()
{
Name = "Model "+ ++count;
}
private string _Name;
public string Name
{
get { return _Name; }
set { _Name = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
You don't need to use a Button in order to select the item. When you click/tap on the item it will get automatically selected.
Then simply bind ListBox.SelectedIndex to your view model property SomeInt and it will update on every selection.
Data binding overview in WPF
You can also get the item itself by binding ListBox.SelectedItem to your view model.
You can handle new values by invoking a handler from the property's set method:
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
private int currentItemIndex;
public int CurrentItemIndex
{
get => this.currentItemIndex;
set
{
this.currentItemIndex = value;
OnPropertyChanged();
// Handle property changes
OnCurrentItemIndexChanged();
}
}
private MyItem currentItem;
public MyItem CurrentItem
{
get => this.currentItem;
set
{
this.currentItem = value;
OnPropertyChanged();
}
}
protected virtual void OnCurrentItemIndexChanged()
{
// Handle the new this.CurrentItemIndex value
}
// Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ItemView .xaml
<UserControl>
<UserControl.DataContext>
<ViewModel />
</UserControl.DataContext>
<ListBox ItemsSource="{Binding MyItems}"
SelectedIndex="{Binding CurrentItemIndex}"
SelectedItem="{Binding CurrentItem}" />
</UserControl>
In my application i have the following MasterViewModel1-class.
public class MasterViewModel1 : ViewModelBase
{
private ObservableCollection<ObservableObject> _MainGrid;
public ObservableCollection<ObservableObject> MainGrid
{
get => _MainGrid;
set
{
_MainGrid = value;
RaisePropertyChanged();
}
}
public ObservableCollection<FilterItem> FilterItems
{
get;
set;
}
public MasterViewModel1()
{
CreateDefaultMenu();
}
public void CreateDefaultMenu()
{
FilterItems = new ObservableCollection<FilterItem>
{
new FilterItem(OnFilterClicked)
{
Content = "Filter"
},
new FilterItem(OnFilterCancelClicked)
{
Content = "Filter aufheben"
}
};
}
public virtual void OnFilterClicked() { }
public virtual void OnFilterCancelClicked() { }
The MasterViewModel1-class is inherited by the TestViewModel-class.
public class TestViewModel : MasterViewModel1
{
private Kunde _NeuerKunde;
public Kunde NeuerKunde
{
get => _NeuerKunde;
set => _NeuerKunde = value;
}
private string _Kundenmatchcode;
public string Kundenmatchcode
{
get => _Kundenmatchcode;
set
{
_Kundenmatchcode = value;
RaisePropertyChanged();
}
}
public TestViewModel()
{
NeuerKunde = new Kunde();
}
}
I use the MasterViewModel1-class and its view for reusable reasons, because in the future there will be many more views which will inherit the MasterViewModel.
Inside the MasterView in need to bind to both, the MasterViewModel, so i have the "Base-Design".
And i need to bind to the "Sub"ViewModel, in this example the TestViewModel.
View of the MasterViewModel1
In the image u can see the MasterView. The red marked region is the place where the TestViewModel (TestView) should be placed. I can't use staticresource!!! It have to be dynamic, so if i instanciate another ViewModel, which also inherites from MasterViewModel1. The red marked region should change depending on the instantiated ViewModel.
I hope it's clear enought.
If u need further informations please ask.
Generally, all public properties of a superclass are visible and accessible via every subclass. You can bind to every public property.
If you want to change the layout or appearance of a view based on the actual implementation or type, you should use a DataTemplate which describes how the view is structured and bound to the model's data.
A simple ContentControl will serve as the dynamic view host.
ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
private ViewModelBase currentView;
public ViewModelBase CurrentView
{
get => this.currentView;
set
{
this.currentView= value;
OnPropertyChanged();
}
}
public ICommand ToggleViewCommand => new RelayCommand(param => this.CurrentView = this.Views.FirstOrDefault(view => view != this.CurrentView));
private List<ViewModelBase> Views { get; }
public MainViewModel()
{
this.Views = new ObservableCollection<ViewModelBase>()
{
new TestViewModel() { Value = "TestViewModel View" },
new AnotherTestViewModel() { Name = "AnotherTestViewModel View" }
}
this.CurrentView = this.Views.First();
}
}
TestViewModel.cs
public class TestViewModel : ViewModelBase
{
private string value;
public string Value
{
get => this.value;
set
{
this.value = value;
OnPropertyChanged();
}
}
}
AnotherTestViewModel.cs
public class TestViewModel : ViewModelBase
{
private string name;
public string Name
{
get => this.name;
set
{
this.name = value;
OnPropertyChanged();
}
}
}
TestView.xaml
<Window>
<Window.DataContext>
<TestViewModel />
</Window.DataContext>
<TextBlock Text="{Binding Value}" />
</Window>
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<!-- Define the views as an implicit (keyless) DataTemplate -->
<DataTemplate DataType="{x:Type TestViewModel}">
<!-- Show a view as a UserControl -->
<TestView />
</DataTemplate>
<DataTemplate DataType="{x:Type AnotherTestViewModel}">
<!-- Or add a elements -->
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<Rectangle Height="80" Width="80" Fill="Red" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Command="{Binding ToggleViewCommand}" Content="Toggle View" />
<!--
Host of the different views based on the actual model type (dynamic view).
The implicit DataTemplates will apply automatically
and show the view that maps to the current CurrentView view model type
-->
<ContentControl Content="{Binding CurrentView}" />
</StackPanel>
</Window>
Here is a class with undefined variable that needs to be passed into the WPF window.
public class SelectedVal<T>
{
public T val {get;set;}
}
Window:
public partial class SOMEDialogue : Window
{
public List<SelectedVal<T>> returnlist { get { return FullList; } }
public List<SelectedVal<T>> FullList = new List<SelectedVal<T>>();
public SOMEDialogue (List<SelectedVal<T>> inputVal)
{
InitializeComponent();
}
}
So here is the question, how can I do this properly to get the T and have a global variable set in my WPF?
Edited (code edited too):
The purpose for the WPF is:
A list of SelectedVal<T> input
Display this input in this WPF
Depend on the T type, user can do something about this input
When finished a return List<SelectedVal<T>> returnlist can be
accessed
This is the basic idea I'm describing. Let me know if you hit any snags. I'm guessing that the search text and the min/max int values are properties of the dialog as a whole. I'm also assuming that there may be a mixture of item types in the collection, which may be an assumption too far. Can you clarify that?
Selected value classes
public interface ISelectedVal
{
Object Val { get; set; }
}
public class SelectedVal<T> : ISelectedVal
{
public T Val { get; set; }
object ISelectedVal.Val
{
get => this.Val;
set => this.Val = (T)value;
}
}
public class StringVal : SelectedVal<String>
{
}
public class IntVal : SelectedVal<int>
{
}
Dialog Viewmodel
public class SomeDialogViewModel : ViewModelBase
{
public SomeDialogViewModel(List<ISelectedVal> values)
{
FullList = values;
}
public List<ISelectedVal> FullList { get; set; }
private String _searchText = default(String);
public String SearchText
{
get { return _searchText; }
set
{
if (value != _searchText)
{
_searchText = value;
OnPropertyChanged();
}
}
}
private int _minInt = default(int);
public int MinInt
{
get { return _minInt; }
set
{
if (value != _minInt)
{
_minInt = value;
OnPropertyChanged();
}
}
}
private int _maxInt = default(int);
public int MaxInt
{
get { return _maxInt; }
set
{
if (value != _maxInt)
{
_maxInt = value;
OnPropertyChanged();
}
}
}
}
.xaml.cs
public SOMEDialogue (List<ISelectedVal> inputValues)
{
InitializeComponent();
DataContext = new SomeDialogViewModel(inputValues);
}
XAML
<Window.Resources>
<DataTemplate DataType="{x:Type local:StringVal}">
<StackPanel>
<Label>Value</Label>
<Label Content="{Binding Val}" />
<Label>Search text:</Label>
<TextBox Text="{Binding DataContext.SearchText, RelativeSource={RelativeSource AncestorType=Window}}" />
<!-- Other stuff -->
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:IntVal}">
<StackPanel>
<Label>Value</Label>
<Label Content="{Binding Val}" />
<Label>Min value:</Label>
<TextBox Text="{Binding DataContext.MinIntVal, RelativeSource={RelativeSource AncestorType=Window}}" />
<Label>Max value:</Label>
<TextBox Text="{Binding DataContext.MaxIntVal, RelativeSource={RelativeSource AncestorType=Window}}" />
<!-- Other stuff -->
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl
ItemsSource="{Binding FullList}"
/>
</Grid>
I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:local="clr-namespace:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.
I have a TreeView with HierarchicalDataTemplate that I'm trying to bind with an ObservableCollection of custom types.
But the HierarchicalDataTemplate's DataType attribute's dropdown list of available types in my Namespace is incomplete, it's missing the TFolderItem custom type, but listing all the other custom types in that same namespace. The namespace is MyProject.Classes, and classes are in plain Classes folder in the project directory.
I don't understand why it's not showing in the XAML code editor dropdown.
public class TFolderItem
{
/*public FolderItem(RemoteDirectoryInfo rdi, WinSCP.Session winscpSession)
{
RDI = rdi;
this.WinSCPSession = winscpSession;
}*/
public TFolderItem(string path, WinSCP.Session winscpSession)
{
RDI = winscpSession.ListDirectory(path);
this.FtpPath = path;
this.WinSCPSession = winscpSession;
}
private WinSCP.Session winscpSession;
public RemoteDirectoryInfo RDI { get; set; }
public string FtpPath { get; set; }
public WinSCP.Session WinSCPSession
{
get { return this.winscpSession; }
set { this.winscpSession = value; }
}
public IList Children
{
get
{
var children = new CompositeCollection();
var subDirItems = new List<TFolderItem>();
var subDirFiles = new List<RemoteFileInfo>();
foreach (RemoteFileInfo rfi in RDI.Files)
{
if (rfi.IsDirectory)
{
subDirItems.Add(new TFolderItem(this.FtpPath + rfi.Name + "/", this.WinSCPSession));
}
else
{
subDirFiles.Add(rfi);
}
}
children.Add(new CollectionContainer
{
Collection = subDirItems
});
children.Add(new CollectionContainer
{
Collection = subDirFiles
});
return Children;
}
}
}
Here is the view's xaml:
<UserControl x:Class="MyProject2.Views.FTPTab"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyProject2.Views"
xmlns:MyProject2Classes="clr-namespace:MyProject2.Classes"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TreeView ItemsSource="{Binding FolderItems}" Height="300" Width="300">
<TreeView.Resources >
<HierarchicalDataTemplate DataType="" ItemsSource="{Binding Childrenx}">
<TextBlock Text="{Binding FtpPathr}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType=":">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
This is the viewmodel:
public class FTPTabViewModel : BindableBase
{
public FTPTabViewModel(string host, WinSCP.Session winscpSession)
{
this.Host = host;
this.FolderItems = new ObservableCollection<TFolderItem>();
this.Session = winscpSession;
this.FolderItems.Add(new TFolderItem("/",Session));
}
private WinSCP.Session session;
private ObservableCollection<TFolderItem> folderItems;
private string host;
public string Host
{
get { return this.host; }
set { this.host = value; }
}
public WinSCP.Session Session
{
get { return session; }
set { this.session = value; }
}
public ObservableCollection<TFolderItem> FolderItems
{
get { return folderItems; }
set { SetProperty(ref this.folderItems, value); }
}
}
It seems that x:type dropdown only displays classes with default constructor.
Adding one in TFolderItem class made it display in x:type dropdown.