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

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 }
};
}
}
}

Related

Lazy Loading TreeViewItem with custom loading method depending on their nested type [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
Let's say I have a collection of employees. The employees can be a senior supervisor, junior supervisor or a trainee.
These 3 types of employees are classes derived from employee. Each Senior supervisor guids a group of junior supervisors. Each junior supervisor guids a group of trainees.
LoadChilds fill the Childs Collection.
Now i want to show their hierarchy from senior to trainee in a treeview by using lazy initialization. I would like call the method LoadChildson TreeViewItem.Exapand event. ( I am open for other ideas.)
Currently i am presenting the different datatypes by a HierarchicalDataTemplate.
<TreeView x:Name="Tree">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type model:Supervisor}" ItemsSource="{Binding Childs}">
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:Employee}" ItemsSource="{Binding Childs}">
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Question: How can I call a method, edit the data und refresh the UI on Expanding a TreeViewItem?
there is definitely a "prettier" way - without writing business logic in event handler in code-behind.
introduce boolean IsExpanded in a view model classes, bind TreeViewItem.IsExpanded to that property and trigger LoadChilds method in setter:
<TreeView x:Name="Tree">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type model:A}" ItemsSource="{Binding Childs}">
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:B}" ItemsSource="{Binding Childs}">
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
public class A
{
public A()
{
// null is a placeholder.
// without any items TreeViewItem will not even show expander
// (Expand event won't work either)
Childs = new ObservableCollection<B>() { null };
}
public string Name { get; set; }
private bool _isExpanded;
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
// when node is expanded, RELOAD!
if (_isExpanded)
LoadChilds();
}
}
public ObservableCollection<B> Childs { get; set; }
public void LoadChilds()
{
Childs.Clear();
Childs.Add(new B() { Name = Guid.NewGuid().ToString() });
}
}
B is almost identical in this test example, but I suppose LoadChilds logic will be different in a real app
public class B
{
public B()
{
Childs = new ObservableCollection<B>() { null };
}
public string Name { get; set; }
private bool _isExpanded;
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
if (_isExpanded)
LoadChilds();
}
}
public ObservableCollection<B> Childs { get; set; }
public void LoadChilds()
{
Childs.Clear();
Childs.Add(new B() { Name = Guid.NewGuid().ToString() });
}
}
You could define an interface that both A and B implements and cast the DataContext of the expanded TreeViewItem to this type:
public interface ICommon
{
string Name { get; set; }
void LoadChilds(DBConnection connection);
}
public class A : ICommon
{
public class A()
{
Childs = new ObservableCollection<B>();
}
public string Name { get; set; }
public ObservableCollection<B> Childs { get; set; }
public void LoadChilds(DBConnection connection) //Type is unimportant
{
// adding childs
}
}
public class B : ICommon
{
public class B()
{
Childs = new ObservableCollection<B>(); //Yes, rekursiv
}
public string Name { get; set; }
public ObservableCollection<B> Childs { get; set; }
public void LoadChilds(DBConnection connection)
{
// adding childs
}
}
private void TreeView_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = e.OriginalSource as TreeViewItem;
ICommon obj = tvi.DataContext as ICommon;
if (obj != null)
obj.LoadChilds(...);
}

WPF MVVM - items in Treeview not being updated after being openened

I have a TreeView which contains items of the type TPDItem, each TPDItem has a ObservableCollection of TPDItems which are displayed in the following manner:
TPDItem Hierarchy
The level shows which items are parents of which children, 1.1, 1.2 and 1.3 are children of the item with Level 1.
If i tick the checkbox Export, I want set the export value of that item, and it's children (and it's children children) recursively.
This is my TPDItem class:
public class TPDItem : INotifyPropertyChanged
{
public List<string> LevelArr { get; }
public string Level { get; }
public string _12NC { get; }
private string pn;
public string Description { get; }
private ObservableCollection<TPDItem> children = new ObservableCollection<TPDItem>();
private bool isExported = true;
public bool IsExported
{
get { return isExported; }
set
{
SetExported(value);
OnPropertyChanged("IsExported");
}
}
public string PN
{
get { return pn; }
set { pn = value; }
}
public ObservableCollection<TPDItem> Children
{
get
{
return children;
}
}
public void SetExported(bool exported)
{
isExported = exported;
foreach (TPDItem item in Children)
{
item.SetExported(exported);
}
}
}
And this is my relevant TreeView XAML code:
<TreeView ItemsSource="{Binding Hierarchy}" Margin="10,0,10,0" Height="243" >
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type models:TPDItem}">
<Grid >
<TextBlock Text="{Binding Level}"/>
<TextBlock Text="{Binding _12NC}" Margin="{Binding Margins._12NC}"/>
<TextBlock Text="{Binding PN}" Margin="{Binding Margins.PN}"/>
<TextBlock Text="{Binding Description}" Margin="{Binding Margins.Description}"/>
<CheckBox Content="Export" Margin="{Binding Margins.CheckBox}" IsChecked="{Binding IsExported, Mode=TwoWay}" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
However, the Checkbox in the children only gets updated to their parent's value if that child has not been expanded yet. After creating the tree, If I untick the top item's checkbox, the whole list gets unticked. However, If I expand and close a child, and then tick their parent's checkbox, they don't get updated visually.
Please let me know if you need more information.
Because you directly call SetExported on the children, you are skipping the part of the setter that calls OnPropertyChanged. Note that SetExported sets the backing variable isExported, but never uses the setter on the public property IsExported, which is what would trigger the visual update.
Try this:
public void SetExported(bool exported)
{
isExported = exported;
foreach (TPDItem item in Children)
{
// this will call the SetExported method, but will also trigger OnPropertyChanged
item.IsExported = exported
}
}
Also, making the SetExported method private instead of public would avoid this type of bug.

Databinding a nested List property in WPF

I am using the following XAML code to display a list of checked list boxes.
<ListBox x:Name="lbxProjects" ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ListBox x:Name="lbxUnits" ItemsSource="{Binding Units}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox Content="{Binding unit.Name}" IsChecked="{Binding isSelected}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The data model is as follows
public class ProjectsListBox
{
public Project project { get; set; }
public List<UnitsCheckBox> Units = new List<UnitsCheckBox>();
public ProjectsListBox(Project project)
{
this.project = project;
foreach(var d in project.Documents)
{
Units.Add(new UnitsCheckBox(d));
}
}
}
public class UnitsCheckBox : INotifyPropertyChanged
{
public Document unit { get; set; }
private bool isselected = true;
public bool isSelected
{
get { return isselected; }
set
{
isselected = value;
NotifyPropertyChanged("isSelected");
}
}
public UnitsCheckBox(Document d)
{
unit = d;
}
}
I am assigning the data source for the parent listbox like
lbxProjects.DataContext = projectsList;
The code creates the child list boxes but not the checkboxes inside the child list boxes. What am i missing?
How should WPF resolve unit.Name?
If the type UnitsCheckBox contains a Name property, then the CheckBox's Content should be bound to Name:
Content="{Binding Name}"
You should always specify the type of your DataTemplate:
<DataTemplate DataType="{x:Type local:UnitsCheckBox}" ...>
Those are the probable problems but I can't be sure unless you give us the UnitsCheckBox code.

Adding TreeView in WPF with context menu in subitems

In my WPF application, I want to add a TreeView control. The tree view control needs to be populated with items from database. So I bind the ItemsSource property to string collection.
Every item in the tree control can have from 0 to 32 child items. Again these items need to be binded. Each of these sub items should have a context menu with two options "Rename" and "Delete". How can I do this in WPF?
There are a few ways to do this. Here's one way that applies the context menu using a trigger that is bound to a property IsLeaf on the underlying view model.
MainWindow.xaml:
<Window x:Class="WpfScratch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<!-- the context menu for all tree view items -->
<ContextMenu x:Key="TreeViewItemContextMenu">
<MenuItem Header="Rename" />
<MenuItem Header="Delete" />
</ContextMenu>
<!-- the data template for all tree view items -->
<HierarchicalDataTemplate x:Key="TreeViewItemTemplate" ItemsSource="{Binding Nodes}">
<TextBlock x:Name="TextBlock" Text="{Binding Text}" />
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsLeaf}" Value="True">
<Setter TargetName="TextBlock" Property="ContextMenu" Value="{StaticResource TreeViewItemContextMenu}" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</Window.Resources>
<!-- the treeview -->
<TreeView DataContext="{Binding TreeView}"
ItemsSource="{Binding Nodes}"
ItemTemplate="{StaticResource TreeViewItemTemplate}">
</TreeView>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowModel(
new MainWindowTreeViewModel(
new MainWindowTreeViewNodeModel(
"1",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C")),
new MainWindowTreeViewNodeModel(
"2",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C")),
new MainWindowTreeViewNodeModel(
"3",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C"))));
}
}
MainWindowModel.cs:
public class MainWindowModel
{
public MainWindowModel(MainWindowTreeViewModel treeView)
{
TreeView = treeView;
}
public MainWindowTreeViewModel TreeView { get; private set; }
}
public class MainWindowTreeViewModel
{
public MainWindowTreeViewModel(params MainWindowTreeViewNodeModel[] nodes)
{
Nodes = nodes.ToList().AsReadOnly();
}
public ReadOnlyCollection<MainWindowTreeViewNodeModel> Nodes { get; private set; }
}
public class MainWindowTreeViewNodeModel
{
public MainWindowTreeViewNodeModel(string text, params MainWindowTreeViewNodeModel[] nodes)
{
Text = text;
Nodes = nodes.ToList().AsReadOnly();
}
public string Text { get; private set; }
public ReadOnlyCollection<MainWindowTreeViewNodeModel> Nodes { get; private set; }
public bool IsLeaf { get { return Nodes.Count == 0; } }
}

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