HierarchicalDataTemplate bound to a TreeView does not produce anything - c#

I am trying to bind a hierarchical structure to a TreeView using Data Binding, but nothing is displayed in the TreeView.
XAML code in the Control:
<UserControl x:Class="CQViewer.Views.HierarchyView"
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:h="clr-namespace:CQViewer.Hierarchy"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TabControl Margin="5, 0, 5, 5" Grid.Column="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<TabItem Header="Repository">
<TreeView Margin="5, 5, 5, 5" HorizontalAlignment="Stretch" ItemsSource="{Binding Path=Nodes}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type h:HierarchyNode}"
ItemsSource="{Binding ChildNodes}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</TabItem>
<TabItem Header="Libraries">
<ListBox Margin="5, 5, 5, 5" HorizontalAlignment="Stretch" />
</TabItem>
</TabControl>
</Grid>
</UserControl>
The class that I am using:
using System.Collections.Generic;
namespace CQViewer.Hierarchy
{
class HierarchyNode
{
#region Fields
private IList<HierarchyNode> childNodes_;
#endregion
#region Construction/Deconstruction/Initialisation
/// <summary>
/// Constructor
/// </summary>
public HierarchyNode (string Name)
{
this.Name = Name;
childNodes_ = new List<HierarchyNode> ();
}
#endregion
#region Properties
public string Name { get; set; }
public IList<HierarchyNode> ChildNodes
{
get { return childNodes_; }
set { childNodes_ = value; }
}
#endregion
}
}
What I want to do is populate the TreeView using the ChildNodes in the HierarchyNode objects using the HierarchicalDataTemplate, while displaying a TextBlock for the name of the current object. The root HierarchyNode is created in the constructor of the ViewModel. Data Binding in this UserControl works, since binding a List<> to the ListBox works fine.
What is wrong with what I am doing, and why it is not working?
EDIT: The ViewModel used for the View
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CQViewer.Hierarchy;
namespace CQViewer.ViewModels
{
class HierarchyViewModel : ViewModelBase
{
#region Fields
#endregion
#region Construction/Deconstruction/Initialisation
/// <summary>
/// Constructor
/// </summary>
public HierarchyViewModel ()
{
Nodes = new HierarchyNode ("Test1");
HierarchyNode Node2 = new HierarchyNode ("TestX");
Node2.ChildNodes.Add (new HierarchyNode ("TestY"));
Nodes.ChildNodes.Add (Node2);
Nodes.ChildNodes.Add (new HierarchyNode ("Test3"));
}
#endregion
#region Properties
public HierarchyNode Nodes { get; set; }
#endregion
#region Public Access Interface
/// <summary>
/// Creates/Recreates the hierarchy
/// </summary>
public void InitializeHierarchy ()
{
// TODO Clear the tree view
}
/// <summary>
/// Gets the folder currently selected in the tree view hierarchy
/// </summary>
public string GetCurrentlySelected ()
{
return "$/";
}
#endregion
#region Private Functions
#endregion
}
}

The problem is that ItemsSource property of TreeViewItem expects a collection where as HierarchyViewModel.Nodes property is of type HierarchyNode. I suggest to change it to ObservableCollection<HierarchyNode>. i.e.:
public ObservableCollection<HierarchyNode> Nodes { get; set; }
Your solution with ItemsSource="{Binding Nodes.ChildNodes}" works partially because a tree view will not display a root node (Test1 in your case).

I have managed to get this to work by changing the TreeView code to the following:
<TreeView Margin="5, 5, 5, 5" HorizontalAlignment="Stretch">
<TreeViewItem Header="{Binding Nodes.Name}" ItemsSource="{Binding Nodes.ChildNodes}">
<TreeViewItem.Resources>
<HierarchicalDataTemplate DataType="{x:Type h:HierarchyNode}"
ItemsSource="{Binding ChildNodes}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeViewItem.Resources>
</TreeViewItem>
</TreeView>
But I am sure that there is a better way to handle this.

Related

Avalonia UI Error in binding "Could not find CLR property"

I am trying to create an MVVM application which pulls data from an API and puts the data into lists which are used to navigate the program.
The issue I'm having is that the list produces this error and I cannot figure out why:
Binding: Error in binding to "Avalonia.Controls.TextBlock"."Text":
"Could not find CLR property 'name' on 'attributes'"
For context, the 'attributes' class contains the 'name' variable and I have confirmed that the name variable is populated beforehand (the program printed the name variable before I moved onto trying to form a list).
XAML code (MainWindow.xaml):
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:RT_Stream_App.ViewModels;assembly=RT_Stream_App"
Icon="resm:RT_Stream_App.Assets.avalonia-logo.ico"
Title="RT Stream App">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<ListBox Items="{Binding CompanyList}" HorizontalAlignment="Left" Width="512" Height="512" Margin="20,20,0,10" VerticalAlignment="Top">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel DataContext="attributes">
<TextBlock Text="{Binding name}" TextAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/> -->
</Window>
MainWindowViewModel.cs
namespace RT_Stream_App.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
// use => instead of = for assigning
// public string Greeting => "I am testing!";
public companies.APIData siteList => loadCompanies();
//public string Greeting => TestLoop(siteList);
public ObservableCollection<companies.companyData> CompanyList => siteList.data;
public companies.APIData loadCompanies()
{
// This takes the API data for companies and converts it into a useable class
companies.APIData toReturn = JsonConvert.DeserializeObject<companies.APIData>(new WebClient().DownloadString("https://svod-be.roosterteeth.com/api/v1/channels"));
return toReturn;
}
}
}
Class data (companies.cs):
namespace RT_Stream_App.Classes
{
public class companies
{
/// <summary>
/// Root of the JSON
/// </summary>
public class APIData
{
public ObservableCollection<companyData> data = new ObservableCollection<companyData>();
}
/// <summary>
/// A class that holds the data for each company (Name and link mostly)
/// </summary>
public class companyData
{
public attributeData attributes = new attributeData();
public linkData links = new linkData();
}
/// <summary>
/// Contains the company name
/// </summary>
public class attributeData
{
public string name { get; set; }
}
/// <summary>
/// Contains link data for the next step
/// </summary>
public class linkData
{
public string shows { get; set; }
}
}
}
What am I doing wrong and what needs to be changed?
Update: I have tried changing the DataTemplate in the XAML to the following:
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding attributes.name}" TextAlignment="Center" />
</StackPanel>
</DataTemplate>
Which produces this error:
Binding: Error in binding to "Avalonia.Controls.TextBlock"."Text":
"Could not find CLR property 'attributes'
<DataTemplate>
<StackPanel DataContext="{Binding attributes}">
<TextBlock Text="{Binding name}" TextAlignment="Center" />
</StackPanel>
</DataTemplate>
Produces this error:
Binding: Error in binding to
"Avalonia.Controls.StackPanel"."DataContext": "Could not find CLR
property 'attributes'
Fix update: From Kekekeks answer, I figured out why my program wasn't working and now lists load. Due to using JSON.NET, I was worried about using constructors but I made the following change to all of my classes and the program displays the intended list
Class data (companies.cs) Updated:
public class APIData
{
public APIData()
{
this.data = new ObservableCollection<companyData>();
}
public ObservableCollection<companyData> data {
get;
set;
}
}
You are setting a string "attributes" as your DataContext. Then Binding can't find the property "name" on System.String.
Remove DataContext="attributes" and replace your binding with Text="{Binding attributes.name}"

WPF Window hide Content DependencyProperty

I want to create a Window which redeclares it's own DependencyProperty named Content.
public partial class InfoWindow : Window
{
public static new readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(InfoWindow), new PropertyMetadata(null));
public object Content
{
get { return GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
}
And XAML bind this property
<ContentControl Content="{Binding ElementName=_this, Path=Content}" />
It works fine, just the Visual Studio Designer complains Logical tree depth exceeded while traversing the tree. This could indicate a cycle in the tree.
Is there any way how to tell the Designer that binding is to the InfoWindow.Content and not Window.Content? Or is it a bad idea hide the property and should I renamed my property?
What I am trying to achieve here is the idea of dynamically defining the Buttons that are used to bring up different views for navigating to different forms. (See below: )
The link between the View and View Models are setup inside the Dictionary View_ViewModel which is used to identify the view to set for the Current view when the button is pressed.
(Note: I have tried to use the most basic objects avoiding IOC containers and such like, so as to make it easier to understand the code)
The most important thing to remember is to set the DataContext correctly otherwise you will get the Logical tree depth exceeded while traversing the tree. Error. You could either do this in the Code behind of the View or inside the XAML.
Example:
public partial class SetupForm : UserControl
{
public SetupForm()
{
InitializeComponent();
DataContext = new SetupFormVM();
}
}
OR
<UserControl.DataContext>
<SaleVM:SalesEntryVM />
</UserControl.DataContext>
Here is a code snippet which probably explains it more clearly and probably answers your question.
The view model defines the how many buttons and views you want in the Main Window. This is achieved by having ItemsControl binding to a list in View Model class.
The Button command is bounded to ICommand ChangeViewCommand property in the View Model class which evaluates the Button pressed and .calls ViewChange method which changes the CurrentView (this is bound to the Content in the XAML)
This is what my main Window xaml looks like.
<Window x:Class="MyNameSpace.Views.ApplicationWindow"
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:local="clr-namespace:MyNameSpace.Views"
mc:Ignorable="d"
Title="ApplicationWindow" Height="Auto" Width="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition /> -------------------> repeated five times
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition /> -------------------------> repeated five times
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" >
<!-- Bind to List of Pages -->
<ItemsControl ItemsSource="{Binding ControlItemsNamesList}" DockPanel.Dock="Top" >
<!-- Stack the buttons horizontally --> The list contains the labels to assign to the buttons
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate> ---------------------------------------> This to stack the buttons Horizontally
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- This looks at the list items and creates a button with ControlName -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding ControlName}"
Command="{Binding DataContext.ChangeViewCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" ------> This is important for the Buttons to work Window or ContentControl.
CommandParameter="{Binding }"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
<ContentControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="5" Grid.RowSpan="4" Content="{Binding CurrentView}"/> ---------> This is where I want the new Windows to appear when I click the button
</Grid>
This is what one of my User control xaml looks like that will appear when I click the button in the Main Window.
<UserControl x:Class="MyNameSpace.Views.SetupForm"
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"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition /> ------------------- repeated five times
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/> ------------------- repeated five times
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Grid.Column="0" Grid.RowSpan="5" Background="AliceBlue" Margin="0,0,0,0" >
<!-- Bind to List of Pages -->
<ItemsControl ItemsSource="{Binding ControlItemsNamesList}" DockPanel.Dock="Left" >
<!-- Stack the buttons horizontally -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- This looks at the list items and creates a button with ControlName -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding ControlName}"
Command="{Binding DataContext.ChangeViewCommand, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}"
CommandParameter="{Binding }"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
<ContentControl Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="5" Grid.RowSpan="4" Content="{Binding CurrentView}"/>
</Grid>
</UserControl>
This is the ViewModel for the Main window:
using Products.MVVMLibrary;
using Products.MVVMLibrary.Interfaces;
using MyNameSpace.Views;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
namespace MyNameSpace.ViewModel
{
public class ApplicationVM : ObservableObject
{
private MyNameSpace IControlItem currentNavigationItem;
private MyNameSpace ContentControl currentView;
private MyNameSpace List<IControlItem> NavigationList;
private MyNameSpace ICommand changeViewCommand;
private MyNameSpace ViewConverter viewDictionary;
private MyNameSpace Dictionary<string, LinkViewToViewModel> View_ViewModel;
public ApplicationVM()
{
viewDictionary = new ViewConverter();
View_ViewModel = new Dictionary<string, LinkViewToViewModel>();
NavigationList = new List<IControlItem>();
InitialiseLists();
}
private MyNameSpace void AddControlNavigationItems(string name, ContentControl view, ObservableObject viewModel)
{
View_ViewModel.Add(name, new LinkViewToViewModel(view, viewModel));
IControlItem item = (IControlItem)viewModel;
NavigationList.Add(item);
}
private MyNameSpace void InitialiseLists()
{
AddControlNavigationItems("Sales", new SalesForm(), new SalesEntryVM());
AddControlNavigationItems("Purchases", new PurchaseEntryForm(), new PurchasesVM());
AddControlNavigationItems("Setup", new SetupForm(), new SetupFormVM());
//Use the property instead which creates the instance and triggers property change
CurrentViewModel = (IControlItem)View_ViewModel[View_ViewModel.Keys.ElementAt(0)].ViewModel;
CurrentView = View_ViewModel[View_ViewModel.Keys.ElementAt(0)].View;
}
public List<IControlItem> ControlItemsNamesList
{
get => NavigationList;
}
/// <summary>
/// Provides a list of names for Navigation controls to the control item
/// </summary>
public Dictionary<string, LinkViewToViewModel> ApplicationViews
{
get
{
return View_ViewModel;
}
set
{
View_ViewModel = value;
}
}
public ContentControl CurrentView
{
get
{
return currentView;
}
set
{
currentView = value;
OnPropertyChanged("CurrentView");
}
}
public IControlItem CurrentViewModel
{
get
{
return currentNavigationItem;
}
set
{
if (currentNavigationItem != value)
{
currentNavigationItem = value;
OnPropertyChanged("CurrentViewModel");
}
}
}
/// <summary>
/// This property is bound to Button Command in XAML.
/// Calls ChangeViewModel which sets the CurrentViewModel
/// </summary>
public ICommand ChangeViewCommand
{
get
{
if (changeViewCommand == null)
{
changeViewCommand = new ButtonClick(
p => ViewChange((IControlItem)p), CanExecute);
}
return changeViewCommand;
}
}
#region Methods
private MyNameSpace void ViewChange(IControlItem viewname)
{
foreach (KeyValuePair<string, LinkViewToViewModel> item in View_ViewModel)
{
if (item.Key == viewname.ControlName)
{//Set the properties of View and ViewModel so they fire PropertyChange event
CurrentViewModel = (IControlItem)item.Value.ViewModel;
CurrentView = item.Value.View;
break;
}
}
}
private MyNameSpace bool CanExecute()
{
return true;
}
#endregion
}
}
Other classes
public class LinkViewToViewModel
{
public LinkViewToViewModel(ContentControl view, ObservableObject viewModel)
{
View = view;
ViewModel = viewModel;
}
public ContentControl View { get; set; }
public ObservableObject ViewModel { get; set; }
}

Expanding/selecting a treeview node when it is not currently visible

I have a requirement to change the selected node in a treeview which is hosted in a separate tab.
Further more, if the parent node is not expanded, I wish to expand the node.
After about an hour of fruitless searching through SO, Google, et al, I have decided to post a question.
I can find and expand the required node when it is all visible, but when the treeveiw is obscured by another tab item, it doesn't update. I'm also not entirely sure that the item is being 'selected' - in the debugger it says ISelected is true, and the IsExpanded property of the parent is also true.
I have simplified down my actual problem in the following lines of code:
XAML (Tab control which has two items, one is a button to reproduce the problem, and a treeview which should be updated):
<Window x:Class="TreeviewTest.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">
<Grid>
<TabControl>
<TabItem Header="Select">
<Button Content="Select Delta!" Click="ButtonBase_OnClick" />
</TabItem>
<TabItem Header="Tree">
<TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</TabItem>
</TabControl>
</Grid>
MainWindow code:
namespace TreeviewTest
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
public ObservableCollection<TreeNode> Nodes { get; set; }
public ICollectionView NodesDisplay { get; set; }
public MainWindow()
{
InitializeComponent();
Nodes = new ObservableCollection<TreeNode>
{
new TreeNode(new List<TreeLeaf>
{
new TreeLeaf{Name = "Alpha"},
new TreeLeaf{Name = "Beta"}
}){ Name = "One" },
new TreeNode(new List<TreeLeaf>
{
new TreeLeaf{Name = "Delta"},
new TreeLeaf{Name = "Gamma"}
}){ Name = "Two" }
};
NodesDisplay = CollectionViewSource.GetDefaultView(Nodes);
DataContext = this;
}
public class TreeNode
{
public string Name { get; set; }
public ObservableCollection<TreeLeaf> Children { get; private set; }
public ICollectionView ChildrenDisplay { get; private set; }
public TreeNode(IEnumerable<TreeLeaf> leaves)
{
Children = new ObservableCollection<TreeLeaf>(leaves);
ChildrenDisplay = CollectionViewSource.GetDefaultView(Children);
}
}
public class TreeLeaf
{
public string Name { get; set; }
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
EnsureCanIterateThroughCollection(treTest);
var rootLevelToSelect = Nodes.First(x => x.Name == "Two");
TreeViewItem root = treTest.ItemContainerGenerator.ContainerFromItem(rootLevelToSelect) as TreeViewItem;
EnsureCanIterateThroughCollection(root);
var leafLevelToSelect = rootLevelToSelect.Children.First(x => x.Name == "Delta");
TreeViewItem leaf = root.ItemContainerGenerator.ContainerFromItem(leafLevelToSelect) as TreeViewItem;
if (!root.IsExpanded)
root.IsExpanded = true;
leaf.IsSelected = true;
ReflectivelySelectTreeviewItem(leaf);
}
//Got this from another SO post - not sure is setting IsSelected on the node is actually doing what I think it is...
private static void ReflectivelySelectTreeviewItem(TreeViewItem node)
{
MethodInfo selectMethod = typeof(TreeViewItem).GetMethod("Select", BindingFlags.NonPublic | BindingFlags.Instance);
selectMethod.Invoke(node, new object[] { true });
}
private static void EnsureCanIterateThroughCollection(ItemsControl itemsControl)
{
if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.NotStarted)
ForceGenerateChildContent(itemsControl);
}
private static void ForceGenerateChildContent(ItemsControl itemsControl)
{
itemsControl.ApplyTemplate();
IItemContainerGenerator generator = itemsControl.ItemContainerGenerator;
GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
for (int i = 0; i < itemsControl.Items.Count; i++)
{
DependencyObject dp = generator.GenerateNext();
generator.PrepareItemContainer(dp);
}
}
}
}
}
Also - another XAML snippet which does the same thing, but where the treeview is visible - you should be able to see the treeview expand and the item selected
<Window x:Class="TreeviewTest.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">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Select Delta!" Click="ButtonBase_OnClick" />
<TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest" Grid.Row="1">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
I would appreciate any help - My understanding of treeview's and WPF and databinding is that it doesn't work as nicely as something that gives you IsSynchronisedWithCurrentItem, which is why I am trying to handle updates to the treeview manually and am also trying to select items in the tree programmatically. If I am wrong on that front I would love a pointer to show me how to do it in a more 'WPF' way!
When a tab page is not visible, the controls are not created. Only once you switch to it are the controls created. Add a Boolean to your TreeNode viewmodel and bind the IsSelected property.
TreeNodeVm.cs:
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace TreeViewSelectTest
{
public class TreeNodeVm : NotificationObject
{
private TreeNodeVm Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
RaisePropertyChanged(() => IsSelected);
}
}
private bool _isExpanded = false;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
RaisePropertyChanged(() => IsExpanded);
}
}
public ObservableCollection<TreeNodeVm> Children { get; private set; }
public string Header { get; set; }
public TreeNodeVm()
{
this.Children = new ObservableCollection<TreeNodeVm>();
this.Children.CollectionChanged += Children_CollectionChanged;
}
void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (var newChild in e.NewItems.Cast<TreeNodeVm>())
{
newChild.Parent = this;
}
}
}
public TreeNodeVm(string header, IEnumerable<TreeNodeVm> children)
: this()
{
this.Header = header;
foreach (var child in children)
Children.Add(child);
}
public void MakeVisible()
{
if (Parent != null)
{
Parent.MakeVisible();
}
this.IsExpanded = true;
}
public void Select()
{
MakeVisible();
this.IsSelected = true;
}
}
}
MainWindow.xaml:
<Window x:Class="TreeViewSelectTest.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">
<DockPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Select B1" Click="btSelectB1_Click" />
</StackPanel>
<TabControl>
<TabItem Header="treeview">
<TreeView ItemsSource="{Binding Path=RootNode.Children}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected,Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}" />
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</TabItem>
<TabItem Header="the other item">
<Button />
</TabItem>
</TabControl>
</DockPanel>
</Window>
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TreeViewSelectTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
RootNode = new TreeNodeVm("Root", new[]
{
new TreeNodeVm("A", new [] {
new TreeNodeVm("A1", new TreeNodeVm[0]),
new TreeNodeVm("A2", new TreeNodeVm[0]),
new TreeNodeVm("A3", new TreeNodeVm[0])
}),
new TreeNodeVm("B", new [] {
new TreeNodeVm("B1", new TreeNodeVm[0])
})
});
InitializeComponent();
this.DataContext = this;
}
public TreeNodeVm RootNode { get; private set; }
private void btSelectB1_Click(object sender, RoutedEventArgs e)
{
RootNode.Children[1].Children[0].Select();
}
}
}
When you call TreeNodeVm.Select(), it will update the future state of the visuals. Once the tab page gets switched back, the template is applied and the visuals are created as expanded and selected.

WPF ComboBox selection change on TabItem selection change

I have a combobox in a tab item in MVVM. This tab can be created multiple times in my application (same view, same view model but different instance), so I can switch from one tab to another (but they are tab of the same type).
It works perfectly with every WPF control, but with combobox I have a strange behaviour:
the focus tab, when it loses focus, gets the selected item of the combox box of the tab that the application is focusing on.
If I switch from 2 tabs that are not of the same type everything works correctly, any idea about that? Thanks.
XAML:
<CollectionViewSource x:Key="StatusView" Source="{Binding Path=StatusList}"/>
<ComboBox Name="_spl2Status" Grid.Column="3" Grid.Row="0"
ItemsSource="{Binding Source={StaticResource StatusView}}"
SelectedValue="{Binding Path=CurrentSPL2.ID_SPL2_STATUS, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="FL_TYPE"
DisplayMemberPath="ID_TYPE">
</ComboBox>
VM:
public List<NullableByteEnumType> StatusList
{
get
{
return (SPC_SPL2.SPL2StatusCollection.Skip(1)).ToList();
}
}
private SPC_SPL2 _currentSPL2 = null;
public SPC_SPL2 CurrentSPL2
{
get
{
if (_currentSPL2== null)
Controller.Execute(delegate(IResult result)
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("FL_ACTIVE", true);
parameters.Add("ID_SPL2", _itemcode);
Model.Invalidate(typeof(SPC_SPL2), Filter.GENERIC<SPC_SPL2>(parameters, "ID_SPL2"));
Model.Include<SPC_SPL2>();
if (Model.Appendload(result) == false)
return false;
Debug.Assert(Context.SPC_SPL2.Count == 1);
_currentSPL2= Context.SPC_SPL2.FirstOrDefault();
return result.Successful;
});
return _currentSPL2;
}
set
{
_currentSPL2= value;
OnPropertyChanged(() => CurrentSPL2);
}
}
my tabs are handled in this way:
<Grid>
<Border Grid.Row="0">
<ContentControl
Content="{Binding Path=Workspaces}"
ContentTemplate="{StaticResource MasterWorkspacesTemplate}"
/>
</Border>
</Grid>
where
<DataTemplate x:Key="MasterWorkspacesTemplate">
<TabControl IsSynchronizedWithCurrentItem="True"
BorderThickness="0"
ItemsSource="{Binding}"
SelectedItem="{Binding}"
ItemContainerStyleSelector="{StaticResource TabItemTemplate}"
/>
</DataTemplate>
and workspaces (my viewmodels list) (T is a class who inherit from viewModelBase)
public T CurrentWorkspace
{
get { return WorkspacesView.CurrentItem as T; }
}
private ObservableCollection<T> _workspaces;
public ObservableCollection<T> Workspaces
{
get
{
if (_workspaces == null)
{
_workspaces = new ObservableCollection<T>();
_workspaces.CollectionChanged += _OnWorkspacesChanged;
}
return _workspaces;
}
}
protected ICollectionView WorkspacesView
{
get
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(Workspaces);
Debug.Assert(collectionView != null);
return collectionView;
}
}
I have recreated your problem. But I couldn't find any issue. Please look at the code below and you might get the solustion. Here is my solution.
MyTab view model
public class MyTab : ViewModelBase
{
#region Declarations
private ObservableCollection<string> statusList;
private string selectedStatus;
#endregion
#region Properties
/// <summary>
/// Gets or sets the header.
/// </summary>
/// <value>The header.</value>
public string Header { get; set; }
/// <summary>
/// Gets or sets the content.
/// </summary>
/// <value>The content.</value>
public string Content { get; set; }
/// <summary>
/// Gets or sets the status list.
/// </summary>
/// <value>The status list.</value>
public ObservableCollection<string> StatusList
{
get
{
return statusList;
}
set
{
statusList = value;
NotifyPropertyChanged("StatusList");
}
}
/// <summary>
/// Gets or sets the selected status.
/// </summary>
/// <value>The selected status.</value>
public string SelectedStatus
{
get
{
return selectedStatus;
}
set
{
selectedStatus = value;
NotifyPropertyChanged("SelectedStatus");
}
}
#endregion
}
MainViewModel view model
public class MainViewModel : ViewModelBase
{
#region Declarations
private ObservableCollection<MyTab> tabs;
private MyTab selectedTab;
#endregion
#region Properties
/// <summary>
/// Gets or sets the tabs.
/// </summary>
/// <value>The tabs.</value>
public ObservableCollection<MyTab> Tabs
{
get
{
return tabs;
}
set
{
tabs = value;
NotifyPropertyChanged("Tabs");
}
}
/// <summary>
/// Gets or sets the selected tab.
/// </summary>
/// <value>The selected tab.</value>
public MyTab SelectedTab
{
get
{
return selectedTab;
}
set
{
selectedTab = value;
NotifyPropertyChanged("SelectedTab");
}
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
/// </summary>
public MainViewModel()
{
this.Tabs = new ObservableCollection<MyTab>();
MyTab tab1 = new MyTab();
tab1.Header = "tab1";
tab1.Content = "Tab 1 content";
ObservableCollection<string> tab1StatusList = new ObservableCollection<string>();
tab1StatusList.Add("tab1 item1");
tab1StatusList.Add("tab1 item2");
tab1StatusList.Add("tab1 item3");
tab1.StatusList = tab1StatusList;
tab1.SelectedStatus = tab1StatusList.First();
this.Tabs.Add(tab1);
MyTab tab2 = new MyTab();
tab2.Header = "tab2";
tab2.Content = "Tab 2 content";
ObservableCollection<string> tab2StatusList = new ObservableCollection<string>();
tab2StatusList.Add("tab2 item1");
tab2StatusList.Add("tab2 item2");
tab2StatusList.Add("tab2 item3");
tab2.StatusList = tab2StatusList;
tab2.SelectedStatus = tab2StatusList.First();
this.Tabs.Add(tab2);
this.SelectedTab = tab1;
}
#endregion
}
And finally this is my XAML
<Window x:Class="ComboboxSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModel="clr-namespace:ComboboxSelectedItem.ViewModels"
Title="MainWindow" Height="350" Width="525">
<Grid Name="mainGrid">
<Grid.DataContext>
<viewModel:MainViewModel />
</Grid.DataContext>
<TabControl
ItemsSource="{Binding Tabs, Mode=TwoWay}"
SelectedItem="{Binding SelectedTab}">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Header}" Margin="0 0 20 0"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<!--Content section-->
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<StackPanel Orientation="Vertical">
<TextBlock
Text="{Binding Content}" />
<ComboBox
ItemsSource="{Binding StatusList}"
SelectedItem="{Binding SelectedStatus}" />
</StackPanel>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Are you absolutely sure that you are creating a new instance of the viewmodel. If not, then the comboboxes are sharing the same collectionviewsource which means that a change in one combobox will be reflected in all comboboxes. I had this same problem myself.
Try declaring the collection view source in code:
CollectionViewSource StatusListViewSource = new CollectionViewSource();
StatusListViewSource.Source = SPL2StatusCollection;
then in xaml change binding to the collectionviewsource:
ItemsSource="{Binding StatusListViewSource.View}"
I converted from vb so it might need some edits.
Does that help?

Problem with MVVM. Dynamic list of grids

I don't know according to MVVM show data on control.
I have a collection of cars.
I want group their by type (eg. Sedan, Combi, Hatchback) and depends of number of types print grids.
So :
5 cars:
2 x sedan, 2 x Combi, 1 x sportcar.
So I want to print 3 grids.
How do it to be ok with MVVM.
Below is some sample code. If your lists of cars can change you should use ObservableCollections instead or implement INotifyPropertyChanged on your viewmodel.
XAML:
<Window x:Class="TestApp.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<Grid>
<ListBox ItemsSource="{Binding Path=CarTypes}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Key}" />
<ListBox ItemsSource="{Binding Path=Value}" DisplayMemberPath="Name" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
</Window>
Code behind:
using System.Collections.Generic;
using System.Windows;
namespace TestApp
{
public partial class Window2 : Window
{
public Window2()
{
InitializeComponent();
DataContext = new CarsVM();
}
}
public class CarsVM
{
public CarsVM()
{
CarTypes = new Dictionary<string, List<Car>>();
// You want to populate CarTypes from some model.
CarTypes["sedan"] = new List<Car>() {new Car("Honda Accord"), new Car("Toyota Camry")};
CarTypes["musclecar"] = new List<Car>() { new Car("Chevy Camaro"), new Car("Dodge Challenger") };
CarTypes["suv"] = new List<Car>() { new Car("Chevy Tahoe") };
}
public Dictionary<string, List<Car>> CarTypes { get; private set; }
}
public class Car
{
public Car(string name)
{
Name = name;
}
public string Name { get; set; }
}
}

Categories

Resources