Custom WPF TreeView - c#

I have a custom class that I would like to bind a WPF TreeView that has three tiers. Each tier needs to be bound like this:
Monitor
--> LCD
--> Samsung 1445 LCD
--> CRT
--> Sony 125 CRT
Here is the example code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SystemInventory sysInventory = new SystemInventory();
//Possibly do something like this.
_myTreeView.DataContext = sysInventory.DeviceGroupInstances;
}
public class SystemInventory
{
public ObservableCollection<DeviceGroup> DeviceGroupInstances { get; set; }
public SystemInventory()
{
DeviceGroupInstances = new ObservableCollection<DeviceGroup>();
DeviceGroupInstances.Add(new DeviceGroup("Monitor"));
}
}
public class DeviceGroup
{
public string DeviceGroupName { get; set; }
public ObservableCollection<DeviceType> DeviceTypeInstances { get; set; }
public DeviceGroup(string deviceGroupName)
{
DeviceTypeInstances = new ObservableCollection<DeviceType>();
DeviceGroupName = deviceGroupName;
if (deviceGroupName == "Monitor")
{
DeviceTypeInstances.Add(new DeviceType("LCD"));
DeviceTypeInstances.Add(new DeviceType("CRT"));
}
}
}
public class DeviceType
{
public string DeviceTypeName { get; set; }
public ObservableCollection<DeviceInstance> DeviceInstances { get; set; }
public DeviceType(string deviceGroupName)
{
DeviceInstances = new ObservableCollection<DeviceInstance>();
DeviceTypeName = deviceGroupName;
if (deviceGroupName == "Monitor")
{
DeviceInstances.Add(new DeviceInstance("Samsung 1445 LCD"));
}
else
{
DeviceInstances.Add(new DeviceInstance("Sony 125 CRT"));
}
}
}
public class DeviceInstance
{
public string DeviceInstanceName { get; set; }
public DeviceInstance(string instanceName)
{
DeviceInstanceName = instanceName;
}
}
}

First FIX
Change CTOR:
public MainWindow()
{
InitializeComponent();
SystemInventory sysInventory = new SystemInventory();
//Possibly do something like this.
this.Content = sysInventory;
}
Second FIX
Use XAML below (add to MainWindow), where {x:Type pc:MainWindow+SystemInventory} used for nested classes
<Window.Resources>
<DataTemplate DataType="{x:Type pc:MainWindow+SystemInventory}">
<TreeView ItemsSource="{Binding DeviceGroupInstances}"/>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type pc:MainWindow+DeviceGroup}" ItemsSource="{Binding DeviceTypeInstances}">
<Label Content="{Binding DeviceGroupName}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type pc:MainWindow+DeviceType}" ItemsSource="{Binding DeviceInstances}">
<Label Content="{Binding DeviceTypeName}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type pc:MainWindow+DeviceInstance}">
<Label Content="{Binding DeviceInstanceName}"/>
</DataTemplate>
</Window.Resources>
About namespaces
Namespace is what you need to know to use classes. If your classes have namespace:
namespace MyCompany.MyProject.MyComponent
{
public class SystemInventory
{
....
}
}
This namespace should be added to XAML with alias to use:
<Window x:Class="MyCompany.MyProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myAlias="clr-namespace:MyCompany.MyProject.MyComponent"
Title="Window1" Height="300" Width="350">
<Window.Resources>
...
Now you can use this classes in XAML like:
<DataTemplate DataType="{x:Type myAlias:DeviceInstance}">
<Label Content="{Binding DeviceInstanceName}"/>
</DataTemplate>

Just translate the property names in the answer Charlie already gave you.
<TreeView ItemsSource="{Binding DeviceGroups}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding DeviceTypeInstances}">
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding DeviceInstances}">
<TextBlock Text="{Binding DeviceInstanceName}"/>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<TextBlock Text="{Binding DeviceTypeName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

You need to use a HierarchicalDataTemplate. There are many examples of this on Stack Overflow (and elsewhere) and if you search around you'll quickly find one. I would create a mock-up to illustrate it, but based on your very low accept rate, I'm not sure it would be worth it. You should look over your past questions and accept answers for all of them.

Related

How to write a DataTemplate that works with a VisualStateGroup in WinUI 3?

I've been trying to pass collections of VisualStateGroup objects to various ContentControls in WinUI 3 (1.1.5) but I keep getting a 'Value does not fall within the expected range.' error. I think I've defined an appropriate DataTemplate, but nothing seems to work. Here's a simple example Page from a new Template Studio Winui 3 project:
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel { get; }
public ShellPage ShellPage { get; }
public MainPage()
{
ViewModel = App.GetService<MainViewModel>();
InitializeComponent();
ShellPage = App.GetService<ShellPage>();
var rootGrid = (ShellPage.IsLoaded) ? VisualTreeHelper.GetChild(ShellPage.NavigationViewControl, 0) as Grid : null;
if (rootGrid != null)
{
var listOfVisualStateGroups = VisualStateManager.GetVisualStateGroups(rootGrid);
if (listOfVisualStateGroups?.Count > 0) cControl.Content = listOfVisualStateGroups[0];
}
}
}
<Page
x:Class="TestCollection.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<ContentControl x:Name="cControl">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="VisualStateGroup">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Page>
All this does is fetch the list of VisualStateGroups attached to the NavigationViewControl of a vanilla WinUI 3 project. The first group in the list is then displayed in the ContentControl cControl by assigning it to Content. But the assignment causes the error.
I gather there is something wrong with the DataTemplate, but I can't figure out what. A similar problem occurs if I try to create a templated ListViewItem with a VisualStateGroup object.
This is a significant refinement of a question I asked a few days ago (please see: Bound ListView won't accept List<VisualStateGroup> as ItemsSource in WinUI 3. Any idea why?, if curious). My purpose in asking is to better understand the mechanics and requirements of templating. Any help is greatly appreciated.
Update: 10/2/2022
I haven't found an explanation for the error but I have found that simply wrapping the VisualStateGroup and VisualState classes lets me use collections of the new classes as I would expect. Thing1 and Thing2 are just copies of VisualStateGroup and VisualState (both of which are sealed and can't be inherited directly).
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel { get; }
public ShellPage ShellPage { get; }
public List<Thing1> Things { get; set; } = new();
public List<Thing2> OtherThings { get; set; } = new();
public MainPage()
{
ViewModel = App.GetService<MainViewModel>();
InitializeComponent();
ShellPage = App.GetService<ShellPage>();
var rootGrid = (ShellPage.IsLoaded) ? VisualTreeHelper.GetChild(ShellPage.NavigationViewControl, 0) as Grid : null;
if (rootGrid != null)
{
var listOfVisualStateGroups = VisualStateManager.GetVisualStateGroups(rootGrid);
foreach (var group in listOfVisualStateGroups) Things.Add(new(group));
if (listOfVisualStateGroups?.Count > 0)
foreach (var state in listOfVisualStateGroups[0].States) OtherThings.Add(new(state));
}
}
}
// copy of VisualStateGroup
public partial class Thing1
{
public VisualState CurrentState { get; }
public CoreDispatcher Dispatcher { get; }
public DispatcherQueue DispatcherQueue { get; }
public string Name { get; }
public IList<VisualState> States { get; }
public IList<VisualTransition> Transitions { get; }
public Thing1(VisualStateGroup group)
{
CurrentState = group.CurrentState;
Dispatcher = group.Dispatcher;
DispatcherQueue = group.DispatcherQueue;
Name = group.Name;
States = group.States;
Transitions = group.Transitions;
}
}
// copy of VisualState
public partial class Thing2
{
public CoreDispatcher Dispatcher { get; }
public DispatcherQueue DispatcherQueue { get; }
public string Name { get; }
public SetterBaseCollection Setters { get; }
public IList<StateTriggerBase> StateTriggers { get; }
public Storyboard Storyboard { get; set; }
public Thing2(VisualState state)
{
Dispatcher = state.Dispatcher;
DispatcherQueue = state.DispatcherQueue;
Name = state.Name;
Setters = state.Setters;
StateTriggers = state.StateTriggers;
Storyboard = state.Storyboard;
}
}
<Page
x:Class="TestCollection.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:views="using:TestCollection.Views"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<StackPanel Orientation="Horizontal">
<ListView Margin="0,0,20,20" BorderThickness="1" BorderBrush="Black" Header="Things (VisualStateGroups)"
ItemsSource="{x:Bind Things}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:Thing1">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Margin="0,0,20,20" BorderThickness="1" BorderBrush="Black" Header="OtherThings (VisualStates)"
ItemsSource="{x:Bind OtherThings}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:Thing2">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Page>
Any idea why an ObservableCollection<Thing1> works but an ObservableCollection<VisualStateGroup> doesn't?

Design Mode Error "Object does not match target type" for Loaded Page Resource

I am currently attempting to mock up data for my view by utilizing Design Data for my ViewModel. Specifically, I have a View front end, and a ViewModel backend for my silverlight application.
When I have mocked up other views, things have worked perfectly. Even in this particular view, the only issue seems to have to be with collections.
Any idea why my "CategoryItem" keeps giving me an error when I try to assign a value to "CategoryName"? I have no idea what is causing the issue...
Code below:
My Design Data:
<vm:MainPageViewModel
xmlns:vm="clr-namespace:WebCatalog.ViewModels"
xmlns:m="clr-namespace:WebCatalog.Models"
SelectedTab="Category 1"
ProjectName="New Project Name"
ShowPopup="False"
IsBusy="False"
CurrentUser="Alex"
>
<vm:MainPageViewModel.Categories>
<m:CategoryItem CategoryName="test"/>
</vm:MainPageViewModel.Categories>
</vm:MainPageViewModel>
My simplified ViewModel:
public class MainPageViewModel {
public string SelectedTab {get;set;}
public string ProjectName {get;set;}
public bool ShowPopup {get;set;}
public bool IsBusy {get;set;}
public string CurrentUser {get;set;}
public ObservableCollection<CategoryItem> Categories {get;private set;}
public MainPageViewModel()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
Finally, my (simplified) view:
<UserControl
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignData Source=../SampleData/MainWindowSampleData.xaml}">
<!-- Decision Categories -->
<StackPanel Width="200" toolkit:DockPanel.Dock="Left" Height="100">
<TextBlock Text="{Binding CurrentUser}">meep</TextBlock>
<ItemsControl Height="100" ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>Itemalkdjfa;ldfj;lakdsjfladfjal;dfjaldfja</TextBlock>
<TextBlock Text="{Binding CategoryName}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
On your VM change
public ObservableCollection<CategoryItem> Categories {get;private set;}
to
public ObservableCollection<CategoryItem> Categories {get;set;}
Otherwise my sample xaml binding looked and worked in design mode like this:
<local:MainVM x:Key="myMainVM">
<local:MainVM.Categories>
<local:CategoryItem Name="Test" />
</local:MainVM.Categories>
</local:MainVM>
...
<ListBox DataContext="{StaticResource myMainVM}"
ItemsSource="{Binding Categories}">
<ItemsControl.ItemTemplate>
<DataTemplate><TextBlock Text="{Binding Name}"/></DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
Where the VM mirrored yours (sans the private set) and the VM was loaded in Xaml on Silverlight and showed the data in design mode.
public class MainVM
{
public ObservableCollection<CategoryItem> Categories { get; set; }
public MainVM()
{
Categories = new ObservableCollection<CategoryItem>();
}
}
public class CategoryItem
{
public string Name { get; set; }
}

How to design a TabPage when the TabControl's ItemsSource is Bind to a List in WPF?

These are my Classes:
class mainViewModel
{
public List<Foo> F { get; set; }
public mainViewModel()
{
F=new List<Foo>()
{
new Foo(new Animal(){Name = "Cat"}),
new Foo(new Animal(){Name = "Dog"}),
new Foo(new Animal(){Name = "Camel"})
};
}
}
public class Foo
{
public Animal Animal { get; set; }
public Foo(Animal animal)
{
Animal = animal;
}
}
public class Animal
{
public string Name { get; set; }
}
And This is my MainWindow Xaml Code:
<TabControl ItemsSource="{Binding Path=F}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Animal.Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="Something 1"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Now obviously I will have a TabControl with one page for each Item in the List F and all the TabControl pages have a TextBlock Something 1 as shown here:
what I want is to design just one of the pages. say add new button to the Camel page named Something 3.
As per the above comments:
Create a specific ViewModel class for each Tab:
public class Tab1: ViewModelBase
{
//... functionality, properties, etc
}
public class Tab2: ViewModelBase
{
//... functionality, properties, etc
}
public class Tab3: ViewModelBase
{
//... functionality, properties, etc
}
Then Create a specific View (usually in the form of UserControls) for each:
<UserControl x:Class"UserControl1" ...>
<!-- UI Elements, etc -->
</UserControl>
<UserControl x:Class"UserControl2" ...>
<!-- UI Elements, etc -->
</UserControl>
<UserControl x:Class"UserControl3" ...>
<!-- UI Elements, etc -->
</UserControl>
Then create DataTemplates for each ViewModel Type and put these UserControls inside them:
Edit: These should be defined in App.xaml under Application.Resources:
<Application ....>
<Application.Resources>
<DataTemplate DataType="{x:Type local:ViewModel1}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<local:UserControl2/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel3}">
<local:UserControl2/>
</DataTemplate>
</Application.Resources>
</Application>
Finally, put an ObservableCollection<ViewModelBase> in your main ViewModel and add these Items:
public ObservableCollection<ViewModelBase> Tabs {get;set;} //Representing each Tab Item
public MainViewModel() //Constructor
{
Tabs = new ObservableCollection<ViewModelBase>();
Tabs.Add(new ViewModel1());
Tabs.Add(new ViewModel2());
Tabs.Add(new ViewModel2());
}

WPF TreeView-How to refresh tree after adding/removing node?

I refer to this article:
WPF TreeView HierarchicalDataTemplate - binding to object with multiple child collections
and modify the tree structure like:
Root
|__Group
|_Entry
|_Source
In Entry.cs:
public class Entry
{
public int Key { get; set; }
public string Name { get; set; }
public ObservableCollection<Source> Sources { get; set; }
public Entry()
{
Sources = new ObservableCollection<Source>();
}
public ObservableCollection<object> Items
{
get
{
ObservableCollection<object> childNodes = new ObservableCollection<object>();
foreach (var source in this.Sources)
childNodes.Add(source);
return childNodes;
}
}
}
In Source.cs:
public class Source
{
public int Key { get; set; }
public string Name { get; set; }
}
In XAML file:
<UserControl.CommandBindings>
<CommandBinding Command="New" Executed="Add" />
</UserControl.CommandBindings>
<TreeView x:Name="TreeView">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="TreeViewItem.IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Root}" ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Path=Name}" IsEnabled="True">
</TextBlock>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Group}" ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Path=Name}" IsEnabled="True">
</TextBlock>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Entry}" ItemsSource="{Binding Items}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" IsEnabled="True">
<TextBlock.ContextMenu>
<ContextMenu >
<MenuItem Header="Add" Command="New">
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:Source}" >
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</TreeView.Resources>
</TreeView>
In UserControl.cs:
public ObservableCollection<Root> Roots = new ObservableCollection<Root>();
public UserControl6()
{
InitializeComponent();
//...Add new node manually
TreeView.ItemsSource = Roots;
}
private void Add(object sender, ExecutedRoutedEventArgs e)
{
Entry ee = (Entry)TreeView.SelectedItem;
Source s3 = new Source() { Key = 3, Name = "New Source" };
ee.Sources.Add(s3);
}
When I click right button on specific node "Entry" to add a new node "Source" under Entry
(call "Add" method), I add a new "Source" object under Entry successfully, but I can't see this new node on treeview. How to refresh treeview when adding/deleting node?
Use ObservableCollection instead of IList if you want to notify the user interface that something in the collection has changed
As far as I'm concerned, changing of type for Items to ObservableCollection<T> will not resolve the problem. You need to implement INotifyPropertyChanged.
I tested both solutions for my tree view, because I faced the same problem.
In my case changing of type from IList to ObservableCollection didn't refreshed GUI. However when I changed my auto property:
public List<SourceControlItemViewBaseModel> Items { get; set; }
to
private IEnumerable<SourceControlItemViewBaseModel> _items;
public IEnumerable<SourceControlItemViewBaseModel> Items
{
get { return _items; }
set
{
_items = value;
OnPropertyChanged();
}
}
Namely, I've implemented INotifyPropertyChanged and that changed the situation. The method that builds the tree structure defines the actual type of Items as new List<T>(), but it works and refreshes the GUI .
Nevertheless my tree was built in pure MVVM pattern without usage code-behind.
I use
<TreeView ItemsSource="{Binding SourceControlStructureItems}" />
and in the view model I use:
currentVm.Items= await SourceControlRepository.Instance.BuildSourceControlStructureAsync(currentVm.ServerPath);
That means I didn't added/removed items, but I rebuilt Node's sub collection.
Use this class and any changes in Sources collection will update/refresh tree in UI.
public class Entry
{
public int Key { get; set; }
public string Name { get; set; }
public ObservableCollection<Source> Sources { get; set; }
public Entry()
{
Sources = new ObservableCollection<Source>();
}
public CompositeCollection Items
{
get
{
return new CompositeCollection()
{
new CollectionContainer() { Collection = Sources },
// Add other type of collection in composite collection
// new CollectionContainer() { Collection = OtherTypeSources }
};
}
}
}

Having problem with treeview and Multilist Hierarchical data

I have the following 2 classes:
public class DeviceGroup
{
public String Name { get; set; }
public ObservableCollection<DeviceGroup> DeviceGroups { get; set; }
public ObservableCollection<Device> Devices { get; set; }
public DeviceGroup()
{
Name = String.Empty;
DeviceGroups = new ObservableCollection<DeviceGroup>();
Devices = new ObservableCollection<Device>();
}
}
public class Device
{
public String Name { get; set; }
}
My main class has an ObservableCollection.
In my Xaml - I can create a treeview easily if I just specify DeviceGroup within my HierachicalDataTemplate, as follows:
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type local:Device}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:DeviceGroup}" ItemsSource="{Binding DeviceGroups}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding DeviceGroups}"/>
</Grid>
The question is: How can I select the Devices collection as well as the DeviceGroup? I'd like the Devices to appear something like Windows Explorer (Directories and Files). Is there a Xaml solution to this problem? Or will I have to create the TreeViewItems in the codebehind.
Thanks.
The only solution I've found so far is within the code behind:
private void LoadTree()
{
foreach (DeviceGroup dg in ttvm.DeviceGroups)
{
TreeViewItem tvi = new TreeViewItem();
tvi.Header = dg;
treeView1.Items.Add(tvi);
AddTreeItems(tvi, dg);
}
}
private void AddTreeItems(TreeViewItem node, DeviceGroup deviceGroup)
{
foreach (DeviceGroup dg in deviceGroup.DeviceGroups)
{
TreeViewItem groupTVI = new TreeViewItem();
groupTVI.Header = dg;
node.Items.Add(groupTVI);
AddTreeItems(groupTVI, dg);
}
foreach (Device device in deviceGroup.Devices)
{
TreeViewItem deviceTVI = new TreeViewItem();
deviceTVI.Header = device;
node.Items.Add(deviceTVI);
}
}
LoadTree() is called after InitilizeComponent. The Xaml changed to:
window resources:
<HierarchicalDataTemplate DataType="{x:Type local:DeviceGroup}" ItemsSource="{Binding DeviceGroups}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
with just a plain treeview in a grid.

Categories