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; } }
}
Related
I am currently displaying a ListView in a Flyout of a DropDownButton. I have an ObservableCollection that is bound to the ListView. During the app initialization, a method that clears the contents of the ObservableCollection and adds some elements to it, is called, this should update the ListView and show some elements on the UI, but an empty container is shown instead.
XAML:
<DropDownButton Content="Super long name that does not fit on the dropdown">
<DropDownButton.Flyout>
<Flyout Placement="Bottom" >
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="3"/>
<Setter Property="CornerRadius" Value="5"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<ListView
ItemsSource="{x:Bind _presenter.ViewModel.OutputDeviceViewModelList, Mode=OneWay}"
DisplayMemberPath="ListName"
SelectedValuePath="SelectedName"
CornerRadius="10"
SelectionMode="None"
IsItemClickEnabled="True"
ItemClick="OnMasterChannelDropdownSelectionChange"
/>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
Main Window (List Initialization call):
public MainWindow()
{
this.InitializeComponent();
_presenter = new Presenter(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
_controller = new Controller();
// Initialize states
_controller.GetDubwareDeviceInfo(); // Trigger a flow that ends up calling the UpdateOutputDevices on the Presenter
}
Presenter code:
public class Presenter
{
public Microsoft.UI.Dispatching.DispatcherQueue DispatcherQueue { get; }
public ViewModel ViewModel { get; } = new();
public void UpdateOutputDevices(List<OutputDeviceDs> outputDeviceDsList)
{
DispatcherQueue.TryEnqueue(() =>
{
ViewModel.OutputDeviceViewModelList.Clear();
ViewModel.OutputDeviceViewModelList.Add(new("", "SelectDevice", "None"));
foreach (OutputDeviceDs outputDeviceDs in outputDeviceDsList)
{
ViewModel.OutputDeviceViewModelList.Add(new(outputDeviceDs.Name, outputDeviceDs.Name, outputDeviceDs.Name));
}
Debug.WriteLine($" Updated output device list, outputDeviceDsList has {outputDeviceDsList.Count} elements and OutputDeviceViewModelList has {ViewModel.OutputDeviceViewModelList.Count} elements");
});
}
}
View model (contains the ObservableCollection):
public class ViewModel
{
public ViewModel() { }
public ObservableCollection<OutputDeviceViewModel> OutputDeviceViewModelList { get; set; } = new();
}
Model:
public class OutputDeviceViewModel
{
public string Id { get; set;}
public string SelectedName { get; set;}
public string ListName { get; set;}
public OutputDeviceViewModel(string id, string selectedName, string listName)
{
Id = id;
SelectedName = selectedName;
ListName = listName;
}
}
When the app is executed Debug print show that the list is populated with 3 elements
But an empty container is shown when the dropdown is open
Is only after interacting with other elements of the page that the list shows three elements.
Currently I have managed force the update of the ListView by creating a non visible ListView, that is bound to the same ObservableCollection, at the same level of the DropDownButton
as such:
<ListView
Visibility="Collapsed"
ItemsSource="{x:Bind _presenter.ViewModel.OutputDeviceViewModelList, Mode=OneWay}"
/>
<DropDownButton Content="Super long name that does not fit on the dropdown">
<DropDownButton.Flyout>
<Flyout Placement="Bottom" >
...
...
...
With this workarround the list shows the 3 elements as soon as the ObservableCollection is updated during intialization
Does anyone has any clue why this this workaround fixes the ListView elements not updating as soon as the list is udpated?
I have hit a pain point in regards to my WPF application, what i am ideally trying to achive here is for a treeview with child items to have a combobox next to each item, and in this combobox to have 3 text values. Such as
ParentItem1 Drop down ComboBox with 3 items
ChildItem1 Drop down ComboBox with 3 items
ChildItem2 Drop down ComboBox with 3 items
ParentItem2 Drop down ComboBox with 3 items
ChildItem1 Drop down ComboBox with 3 items
ChildItem2 Drop down ComboBox with 3 items
ChildItem3 Drop down ComboBox with 3 items
I have fortunately gotten as far as listing some dummy data for the treeview and having a combobox presented, but unfortunately the 3 values do not show. i am using MVVM pattern here where i am binding my data to a viewmodel and vice-versa. here is the code i have so far:
Xaml code:
<Grid.Resources>
<HierarchicalDataTemplate x:Key="servicesKey" DataType="{x:Type src:Service}" ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" Margin="5,10,0,0" />
<ComboBox Name="cmbStatusList"
ItemsSource="{Binding StateList}"
IsTextSearchEnabled="True"
SelectionChanged="cmb_SelectionChanged"
DisplayMemberPath="State"
IsEditable="True"
IsReadOnly="True"
SelectedValuePath="StateID"
Width="150"
Margin="20,0,0,0">
<ComboBox.SelectedValue>
<Binding Path="NewIncident.StateID" Mode="TwoWay" UpdateSourceTrigger="Explicit">
<Binding.ValidationRules>
<validation:ComboBoxRules />
</Binding.ValidationRules>
</Binding>
</ComboBox.SelectedValue>
</ComboBox>
</StackPanel>
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView Name="treeServices"
ContextMenuOpening="ContextMenu_ContextMenuOpening"
ItemsSource="{Binding ServiceModel.Services}"
ItemTemplate="{StaticResource servicesKey}"
VirtualizingPanel.IsVirtualizing="True"
Margin="0,0,10,10">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}"/>
</TreeView.ItemContainerStyle>
</TreeView>
Service View-Model
public class ServiceViewModel : ViewModelBase
{
private List<Service> _services;
public ServiceViewModel()
{
_services = new List<Service>();
}
public List<Service> Services
{
get { return _services; }
set { _services = value; OnPropertyChanged("Services"); }
}
public override void OnChange()
{
throw new NotImplementedException();
}
}
public class Service : INotifyPropertyChanged
{
public Service(Service parent = null)
{
Children = new List<Service>();
}
public Guid Guid { get; set; }
public string Name { get; set; }
public List<Service> Children { get; set; }
public string State { get; set; }
public ServiceState StateID { get; set; }
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Model
var statesmodel = _oakStatusProvider.GetServiceState()
.Select(p => new Service()
{
StateID = p.StateID,
State = p.State
});
_incidentViewModel.StateList = new List<Service>(statesmodel);
this.DataContext = _incidentViewModel;
Get Service Method
public List<ServiceDTO> GetServiceState()
{
List<ServiceDTO> servicestatelist = new List<ServiceDTO>
{
new ServiceDTO { StateID = ServiceState.Normal, State = "Normal" },
new ServiceDTO { StateID = ServiceState.Degraded, State = "Degraded" },
new ServiceDTO { StateID = ServiceState.Critical, State = "Critical" },
};
return servicestatelist;
}
States to be used in CMBbox
public enum ServiceState
{
Normal = 0,
Degraded = 10,
Critical = 20,
}
Result:
Result combobox with treeview
Binding directly to a Combobox works fine, seems to be DataTemplete messing things up?
<ComboBox Name="cmbSstatusList"
ItemsSource="{Binding StateList}"
IsTextSearchEnabled="True"
SelectionChanged="cmb_SelectionChanged"
DisplayMemberPath="State"
IsEditable="True"
IsReadOnly="True"
SelectedValuePath="StateID"
materialDesign:HintAssist.Hint="State"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
Width="150"
Height="45"
FontSize="15" >
<ComboBox.SelectedValue>
<Binding Path="NewIncident.StateID" Mode="TwoWay" UpdateSourceTrigger="Explicit">
<Binding.ValidationRules>
<validation:ComboBoxRules />
</Binding.ValidationRules>
</Binding>
</ComboBox.SelectedValue>
</ComboBox>
Result:
Working CMB
Thanks in advance for any help and assistance :). any questions please let me know.
The Dropdown control is not getting the StateList Property.
You need to name the Window screen and then perform Element Binding to get the StateList Binding work for the Dropdown control.
<Window x:Class="WpfApp11.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp11"
mc:Ignorable="d" x:Name="Window1"
Title="MainWindow" Height="450" Width="800">
And for Combox box Control, use the ElementBinding to Bind the List.
<ComboBox Name="cmbStatusList"
ItemsSource="{Binding ElementName=Window1, Path=DataContext.StateList}"
IsTextSearchEnabled="True"
DisplayMemberPath="State"
IsEditable="True"
IsReadOnly="True"
SelectedValuePath="StateID"
Width="150"
Margin="20,0,0,0">
</ComboBox>
I have an user control (ex: UserCtrlClass)with a tree view inside it
I have View model (ex: OBJViewModel) class for represent the actual items/data display on the tree view
Next I have a Tree View Model (ex: TreeViewModel), which has a list of OBJViewModel objects
Now in the code behind file of the user control, I have instantiated the tree view model class and set as the data context of the user control class
I need a context sensitive menu, which i need to display only when I right click on a specific item in the tree, so I have handled the right click event of the user control class and did the work there
But the commands are not working, The commands are derived from I command and instantiated in TreeViewModel class. i tried to debug my Command.execute was never hit! Any help would be appreciated as I am being a newbie to .net and wpf
TreeViewModel class
<UserControl Name="PFDBUserCtrl" x:Class="BFSimMaster.BFSMTreeview"
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:BFSimMaster.ViewModel"
xmlns:cmd="clr-namespace:BFSimMaster.Commands"
mc:Ignorable="d"
d:DesignHeight="66" d:DesignWidth="300">
<UserControl.Resources>
<!--cmd:ActivateProjectCmd x:Key="CMDActivateProject"/-->
<!--cmd:DeActivateProjectCmd x:Key="CMDDeActivateProject"/-->
</UserControl.Resources>
<DockPanel>
<!-- PF Object Browser TREE -->
<TreeView Name="PFDataBrowser" ItemsSource="{Binding LevelOnePFObjects}" >
<TreeView.Resources>
<ContextMenu x:Key ="ProjectMenu" StaysOpen="true" >
<!-- Text="{Binding Source={StaticResource myDataSource}, Path=PersonName}-->
<!--MenuItem Header="Activate" Command="{Binding Source={StaticResource CMDActivateProject}}" CommandParameter="{Binding Path=PlacementTarget,RelativeSource={RelativeSource AncestorType=ContextMenu}}"/-->
<MenuItem Header="Activate" Command="{Binding DataContext.CMDActivateProject, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" CommandParameter="{Binding Path=PlacementTarget,RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<MenuItem Header="Deactivate" Command="{Binding Source=TVViewModel, Path=CMDDeActivateProject}" CommandParameter="{Binding Path=PlacementTarget,RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<!-- This Style binds a TreeViewItem to a PFObject View Model.-->
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseRightButtonDown" Handler="OnRightButtonDown"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</DockPanel>
Code behind class
using System;
namespace BFSimMaster
{
public partial class BFSMTreeview : UserControl
{
readonly TreeViewItemViewModel mViewModelPFObjBrowserTree;
public BFSMTreeview()
{
InitializeComponent();
WApplication appPF = PFAPIUtils.APIInstance.GetApplication();
WDataObject User = appPF.GetCurrentUser();
// Get raw objects - tree data from a PF database.
//BFPFDataObject userdb = new BFPFDataObject(User,false,"*.IntPrj");
BFPFDataObject userdb = new BFPFDataObject(User, true);
// Create UI-friendly wrappers around the
// raw data objects (i.e. the view-model).
mViewModelPFObjBrowserTree = new TreeViewItemViewModel(userdb);
// Let the UI bind to the view-model.
base.DataContext = mViewModelPFObjBrowserTree;
}
public TreeViewItemViewModel TVViewModel
{
get { return mViewModelPFObjBrowserTree; }
}
private void OnRightButtonDown(object sender, MouseButtonEventArgs e)
{
//MessageBox.Show("Right Clicked on tree view");
if (sender is TreeViewItem)
{
e.Handled = true;
(sender as TreeViewItem).IsSelected = true;
string strObjectType = ((sender as TreeViewItem).Header as PFObjectViewModel).PFDataObject.mThisPFObject.GetClassName().GetString();
switch (strObjectType)
{
case "IntPrj":
(sender as TreeViewItem).ContextMenu = PFDataBrowser.Resources["ProjectMenu"] as System.Windows.Controls.ContextMenu;
(sender as TreeViewItem).ContextMenu.PlacementTarget = (sender as TreeViewItem);
break;
case "Folder":
(sender as TreeViewItem).ContextMenu = PFDataBrowser.Resources["ProjectMenu"] as System.Windows.Controls.ContextMenu;
break;
}
}
}
}
}
the TreeViewModel Class
using System;
namespace BFSimMaster.ViewModel
{
public class TreeViewItemViewModel
{
#region Data
readonly ReadOnlyCollection<PFObjectViewModel> mLevelOnePFObjects;
readonly PFObjectViewModel mRootOfPFObjects;
#endregion // Data
#region Constructor
public TreeViewItemViewModel(BFPFDataObject rootOfPFObjectsA)
{
this.CMDActivateProject = new ActivateProjectCmd();
this.CMDDeActivateProject = new DeActivateProjectCmd();
mRootOfPFObjects = new PFObjectViewModel(rootOfPFObjectsA);
mLevelOnePFObjects = new ReadOnlyCollection<PFObjectViewModel>(
new PFObjectViewModel[]
{
mRootOfPFObjects
});
}
#endregion // Constructor
public ICommand CMDActivateProject { get; set; }
public ICommand CMDDeActivateProject { get; set; }
public ReadOnlyCollection<PFObjectViewModel> LevelOnePFObjects
{
get { return mLevelOnePFObjects; }
}
}
}
A ContextMenu isn't part of the logical tree, so this binding "{Binding DataContext.CMDActivateProject, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" wont work, because it simply has no ancestor of type UserControl. If you dont set the DataContext of the PlacementTarget manuelly, you could try
"{Binding PlacementTarget.DataContext.CMDActivateProject, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
I not 100% sure about the following, but I think this will just work, if youre using the ContextMenu property:
<Treeview>
<Treeview.ContextMenu>
<ContextMenu>
...
<ContextMenu>
<Treeview.ContextMenu>
...
The adventage here is, that you dont have to handle the rightbuttondown event in code-behind, it automatically opens if you right click on your treeview.
I've solved this problem by introducing VM class for MenuItem and setting context menu with ExtendedContextMenu.Items={Binding ContextMenu} attached property. MenuResourcesDictionary is ResourceDictionary.xaml with back-side .cs file (shown below).
To use it for your code you need to add IEnumerable<MenuItemVM> ContextMenu property on your tree model and put commands there (e.g. pass them to MenuItemVM constructor). And in the item style add <Setter Property="ExtendedContextMenu.Items" Value="{Binding DataContext.ContextMenu}" />
public static class ExtendedContextMenu
{
private static readonly StyleSelector _styleSelector = new ContextMenuItemStyleSelector();
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.RegisterAttached("Items",
typeof(IEnumerable<MenuItemVM>),
typeof(ExtendedContextMenu),
new FrameworkPropertyMetadata(default(IEnumerable<MenuItemVM>), ItemsChanged));
public static void SetItems(DependencyObject element, IEnumerable<MenuItemVM> value)
{
element.SetValue(ItemsProperty, value);
}
public static IEnumerable<MenuItemVM> GetItems(DependencyObject element)
{
return (IEnumerable<MenuItemVM>)element.GetValue(ItemsProperty);
}
private static void ItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = (FrameworkElement)d;
var items = (IEnumerable<MenuItemVM>)e.NewValue;
var contextMenu = new ContextMenu();
contextMenu.ItemContainerStyleSelector = _styleSelector;
contextMenu.ItemsSource = items;
return contextMenu;
}
private static void AdjustContextMenuVisibility(ContextMenu menu)
{
menu.Visibility = menu.HasItems ? Visibility.Visible : Visibility.Hidden;
}
}
public class ContextMenuItemStyleSelector : StyleSelector
{
private static readonly MenuResourcesDictionary _resources = new MenuResourcesDictionary();
private static readonly Style _menuItemStyle = (Style)_resources[MenuResourcesDictionary.MenuItemStyleResourceKey];
private static readonly Style _separatorStyle = (Style)_resources[MenuResourcesDictionary.SeparatorStyleResourceKey];
public override Style SelectStyle(object item, DependencyObject container)
{
if (item == MenuItemVM.Separator)
return _separatorStyle;
return _menuItemStyle;
}
}
public sealed partial class MenuResourcesDictionary
{
public const string MenuItemStyleResourceKey = "MenuItemStyleResourceKey";
public const string DynamicMenuItemStyleResourceKey = "DynamicMenuItemStyleResourceKey";
public const string SeparatorStyleResourceKey = "SeparatorStyleResourceKey";
public const string LoadingStyleResourceKey = "LoadingMenuItemStyleResourceKey";
public MenuResourcesDictionary()
{
InitializeComponent();
}
}
Here is XAML styles:
<Style x:Key="{x:Static local:MenuResourcesDictionary.MenuItemStyleResourceKey}">
<Setter Property="MenuItem.Header" Value="{Binding Path=(menuVMs:MenuItemVM.Text)}" />
<Setter Property="MenuItem.Command" Value="{Binding Path=(menuVMs:MenuItemVM.Command)}" />
</Style>
<Style x:Key="{x:Static local:MenuResourcesDictionary.SeparatorStyleResourceKey}" TargetType="{x:Type MenuItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Separator />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MenuItemVM class is more-or-less straighforward, so I don't write it here.
I Found the Answer for TreeView Context menu with and with out DataTemplate here:
TreeView ContextMenu MVVM Binding
MVVM binding command to contextmenu item
Hello and thanks in advance for the assistance,
I have a list of parent objects, in which each parent object has a list of children objects. I would like to display the data in a fashion in which the user may select child objects, press a button and then I will save the selected children objects, along with their parent, likely in xml through serializing the objects with an xml serializer. An idea of what the display should look like is:
<sdk:Label x:Name="ParentLabel" content="ParentNameString" />
<CheckBox x:Name="ChildCheckBox1" Content="ChildNameString" />
<CheckBox x:Name="ChildCheckBox2" Content="ChildNameString" />
<CheckBox x:Name="ChildCheckBox3" Content="ChildNameString" />
<sdk:Label x:Name="ParentLabel2" content="ParentNameString" />
<CheckBox x:Name="ChildCheckBox4" Content="ChildNameString" />
<CheckBox x:Name="ChildCheckBox5" Content="ChildNameString" />
<CheckBox x:Name="ChildCheckBox6" Content="ChildNameString" />
I know that there are options for a checkbox column in the DataGrid control, but would I be able to display the hierarchical relationship there via Header/children? or is there an option for datatemplating in a Listbox control that would allow headings and associated children elements? What would you recommend?
Thanks again for the help.
If you have a single level of Parent and Child you can do something like this.
A simple codebehind with some sample data
namespace SilverlightApplication2
{
public partial class MainPage : UserControl
{
public ObservableCollection<Parent> ParentList { get; set; }
public MainPage()
{
Populate();
InitializeComponent();
}
private void Save_Click(object sender, RoutedEventArgs e)
{
foreach (var child in ParentList
.SelectMany(p => p.Children)
.Where(c => c.IsSelected))
{
//Save the child
Debug.WriteLine(string.Format("Child {0} saved", child.Name));
}
}
private void Populate()
{
ParentList = new ObservableCollection<Parent>();
ParentList.Add(new Parent
{
Name = "John",
Children = new List<Child> { new Child { Name = "Paul" }, new Child { Name = "Pat" } }
});
ParentList.Add(new Parent
{
Name = "Mike",
Children = new List<Child> { new Child { Name = "Bob" }, new Child { Name = "Alice" } }
});
ParentList.Add(new Parent
{
Name = "Smith",
Children = new List<Child> { new Child { Name = "Ryan" }, new Child { Name = "Sue" }, new Child { Name = "Liz" } }
});
}
}
public class Parent
{
public string Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public string Name { get; set; }
public bool IsSelected { get; set; }
}
}
Your xaml would be something like this.
<UserControl x:Class="SilverlightApplication2.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:my="clr-namespace:SilverlightApplication2"
x:Name="MainUserControl"
Width="400"
Height="300"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="ChildTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected}"/>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ParentTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<ListBox ItemsSource="{Binding Path=Children}" ItemTemplate="{StaticResource ChildTemplate}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<StackPanel x:Name="LayoutRoot" Background="White">
<ListBox ItemsSource="{Binding ElementName=MainUserControl, Path=ParentList}" ItemTemplate="{StaticResource ParentTemplate}"/>
</StackPanel>
</UserControl>
This results in a view like this. I have omitted all styling for simplicity, I am sure you can make it more sexy ;)
Now you can in the code behind only process Child with IsSelected true
If you have more than one level... ie..Your Children have children you will have to use the HierarchicalDataTemplate
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 }
};
}
}
}