I am simply trying to display hierarchical Data in a TreeView, but I just can't figure out how to make it display more than the First two Levels. (And i have read almost evry TreeView post, maybe the problem is my (miss)understatement of the Bindings in this case)
I have simplyfied my Datastructure for this test:
public class Node
{
public List<Node> Children { get; set; }
public Node Parent { get; set; }
public string Expression { get; set; }
}
Xaml currently looks like this: (Please notice that I have changed it several times now, but this here is the original state i have come up with: )
<Window x:Class="Klammern_Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Klammern_Test"
Title="MainWindow" Height="439" Width="402">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}">
<TreeViewItem ItemsSource="{Binding Children}" Header="{Binding Expression}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Root}" Margin="12,41,12,12" Name="treeView" />
</Grid>
</Window>
And this is how I am trying to Populate my Treeview:
public partial class MainWindow : Window
{
public Node Root { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
Parser = new StringParser();
Root = Parser.Parse(Tbx_Eingabe.Text);
treeView.Items.Add(PopulateTreeView(Root));
}
private TreeViewItem PopulateTreeView(Node node)
{
TreeViewItem treeViewItem = new TreeViewItem();
treeViewItem.IsExpanded = true;
treeViewItem.Header = node.Expression;
foreach (Node child in node.Children)
{
treeViewItem.Items.Add(new TreeViewItem() { Header = child.Expression });
if (child.Children.Count > 0)
{
PopulateTreeView(child);
}
}
return treeViewItem;
}
}
What am I missing?
EDIT:
After trying around with almulo's hints, I found this with the Snoop-tool, but I can't tell what it means at all, I have found no other red line and no entry to the Binding Errors column at all.
Using a TreeViewItem inside a HierarchicalDataTemplate which is what your TreeView's TreeViewItems will use to create themselves is... confusing.
Inside your HierarchicalDataTemplate you should just add the controls you want the item "header" to have. In this case, I guess it should be a TextBlock since you just wanna show some text.
Then use the HierarchicalDataTemplate.ItemSource property to bind the children of your node.
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Expression}" />
</HierarchicalDataTemplate>
</Window.Resources>
Also, in you code-behind, you shouldn't manipulate the TreeView.Items or the TreeViewItem.Items directly, since you are already using Bindings and the ItemsSource properties.
Instead, remove the PopulateTreeView method and let your Root property work as items source for the TreeView. But in order for this to work, you'll have to notify the view when the Root property changes its value.
To do so, implement the INotifyPropertyChanged interface and fire the PropertyChanged event every time Root changes.
EDIT: The ItemsControl property ItemsSource expects a collection (more specifically, an IEnumerable), so Root needs to be one. Even if it has a single item, like this:
public class MainWindow : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// ...
private void StartButton_Click(object sender, RoutedEventArgs e)
{
Parser = new StringParser();
Root = new Node[] { Parser.Parse(Tbx_Eingabe.Text) };
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Root"));
}
// ...
}
Related
I try to use binding to display Hi in the Text content.
However, when clicking the button, it doesn't work.
Could someone help me to solve the problem?
Thanks.
1.XAML CODE :
<Window x:Class="Wpftest.binding.Window0"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window0" Height="300" Width="300">
<Grid>
<TextBox x:Name="textBox2" VerticalAlignment="Top" Width="168"
Text="{Binding Source= stu, Path= Name, Mode=TwoWay}"/>
</Grid>
</Window>
2.Class :
namespace Wpftest.binding.Model
{
public class student : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
public string Name
{
get { return name; }
set { name = value;
if(this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new
PropertyChangedEventArgs("Name"));
}
}
}
}
}
3.XAML.cs:
namespace Wpftest.binding
{
public partial class Window0 : Window
{
student stu;
public Window0()
{
InitializeComponent();
stu = new student();
}
private void button_Click(object sender, RoutedEventArgs e)
{
stu.Name += "Hi!";
}
}
}
There are many ways to achieve what you need; the correct method depends very much on what style of application you want to create. I'll demonstrate two methods that will require minimal changes from your supplied example:
Method 1
Set the DataContext to stu and bind to the Name property.
XAML.cs
private student stu;
public Window0()
{
InitializeComponent();
stu = new student();
DataContext = stu;
}
XAML code
<TextBox Text="{Binding Path=Name, Mode=TwoWay}"/>
Method 2
Generally you will set the DataContext to some object other than the Window (e.g. the ViewModel if you are following the MVVM pattern), but sometimes you may need to bind a control to some property of the Window. In this case the DataContext can't be used, but you can still bind to a property of the Window by using RelativeSource. See below:
XAML.cs
// note this must be a property, not a field
public student stu { get; set; }
public Window0()
{
InitializeComponent();
stu = new student();
}
XAML code
<TextBox Text="{Binding Path=stu.Name, Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
Hint: if you are having trouble with WPF data binding, then it often helps to look at the debugger output window to see the binding trace messages. And debugging can be further enhanced by adding this namespace to the Window element
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
and then setting the TraceLevel e.g.
<TextBox Text="{Binding Source=stu, diag:PresentationTraceSources.TraceLevel=High}"/>
Basically you need to set DataContext property to your Window.
For example:
public MainWindow()
{
DataContext=new YourViewModel();
}
DataContext of Window is a way to communicate between View(XAML) and ViewModel(C# code)
In addition, you can add DataContext in xaml:
<Window.DataContext>
<local:YourViewModel/>
</Window.DataContext>
Also, instead of handling Click event, you should use Command property of Button. Example can be seen here.
EDIT : Question was not clear enough. In fact there are two of them.
Q1 :
I have a UserControl "CustomView" that is dynamically created with a template:
<Window.Resources>
<DataTemplate DataType="{x:Type my:CustomViewModel}">
<my:CustomView/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Path=CustomList}"/>
Where CustomList is a Property of type ObservableCollection<'CustomViewModel> belonging to MainWindowViewModel, which is the Window's DataContext.
In CustomView's Xaml code, there are some Properties binded to CustomViewModel's Properties. Everything works properly. But when I try to do this in CustomView's code behind :
public CustomView()
{
InitializeComponents();
if (this.DataContext == null) Console.WriteLine ("DataContext is null");
else Console.WriteLine(this.DataContext.GetType().ToString());
}
It is written in Console : 'DataContext is null', even if bindings are working betweeen CustomView and CustomViewModel. Do you know why it's working?
Q2 :
Now, imagine that CustomView has another UserControl (IndexPicker) inside of it. IndexPicker has an associated ViewModel too (IndexPickerViewModel) who's in charge with data access. I need to bind one property ("Index") of this IndexPickerViewModel to the previous CustomViewModel's property "Id". I want to instantiate it in StaticResources and bind it to the CustomViewModel (which I believe is the dataContext according to my previous question):
<UserControl x:Class="MyView.CustomView"
...
<UserControl.Resources>
<DataTemplate DataType="{x:Type myPicker:IndexPickerViewModel}">
<myPicker:IndexPicker/>
</DataTemplate>
<myPicker:IndexPickerViewModel x:Key="pickerViewModel" Index="{Binding Path=Id}/>
</Window.Resources/>
<ContentControl Content={StaticResource pickerViewModel}/>
What I have tried : I tried to make "IndexPickerViewModel" inherit from "DependencyObject" and make "Index" a DependencyProperty. But the following error message shows up :
"System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=Id; DataItem=null; target element is 'IndexPickerViewModel' (HashCode=59604175); target property is 'Index' (type 'Nullable`1')
I believe this is because of what I asked just above. But is it possible to do something like that? If yes, what am I missing? And : Is this a stupid idea?
Thank you in advance for any help.
Now, imagine that CustomView has another UserControl (IndexPicker) inside of it. IndexPicker has an associated ViewModel too (IndexPickerViewModel) who's in charge with data access. I need to bind one property ("Index") of this IndexPickerViewModel to the previous CustomViewModel's property "Id". I want to instantiate it in StaticResources and bind it to the CustomViewModel (which I believe is the dataContext according to my previous question)
If IndexPicker doesn't have an explicitly set datacontext then IndexPicker will inherit the datacontext from it's parent element.
However if IndexPicker does already have a datacontext then you will have to use relative source binding with an ancestor search:
Index="{Binding Id, RelaticeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, FallbackValue={x:Null}}"
Of course you can probably already sense that this is messy. Going after standard properties of a UIElement or Control is quite safe (and common), but when you start going after custom properties then you are introducing dependencies between the child control and its parent (when the child control shouldn't know much of anything about its parent), and you are also bound to start getting binding errors at some stage (hence the use of a fallback value).
It seems that I've asked too early because I've found answers by myself.
Answer to Question1
When you have a UserControl that is dynamically created from a DataTemplate in which it is associated with another object (belonging to a ViewModel or to a Resource), this object is defined as the DataContext of the UserControl. However, you cannot reach it in the UserControl's constructor, you have to wait until the "Loaded" event is raised :
public CustomUserControl()
{
InitializeComponent();
Console.WriteLine(this.DataContext.ToString());
// This doesn't work : DataContext is null
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Console.WriteLine(this.DataContext.ToString());
// or
Console.WriteLine((sender as UserControl).DataContext.ToString());
// this is Ok.
}
Answer to Question2
This is how you do to get a UserControl whose ViewModel is instantiated in a parent UserControl.Resources :
You don't do it.
Instead, you instantiate its ViewModel in its parent ViewModel. Full example :
MainWindow.xaml:
<Window x:Class="MainWindow"
...
xmlns:local="clr-namespace:my_project_namespace"
xmlns:cust="clr-namespace:CustomUserControl;assembly=CustomUserControl"
...>
<Window.Resources>
<DataTemplate DataType="{x:Type cust:CustomControlViewModel}">
<cust:CustomControlView>
</DataTemplate>
<!-- Here are listed all the types inheriting from CustomControlViewModel and CustomControlView.-->
<!-- CustomControlViewModel and CustomControlView are used as "abstract" classes-->
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding Path=CustomVMList}"/>
</Grid>
</Window>
MainWindowViewModel.cs:
namespace my_project_namespace
{
public class MainWindowViewModel
{
public ObservableCollection<CustomControlViewModel> CustomVMList { get; set; }
public MainWindowViewModel()
{
CustomVMList = new ObservableCollection<CustomControlViewModel>();
// Fill in the list...
}
}
}
CustomControlView.xaml
<UserControl x:class="CustomUserControl.CustomControlView"
...
xmlns:my="clr-namespace:IndexPicker;assembly=IndexPicker"
...>
<UserControl.Resources>
<DataTemplate DataType="{x:Type my:IndexPickerViewModel}">
<my:IndexPickerView/>
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}/>
<ContentControl Content="{Binding Path=MyIndexPicker}"/>
</Grid>
</UserControl>
And this is where it's interesting :
CustomControlViewModel.cs:
namespace CustomUserControl
{
public class CustomControlViewModel : INotifyPropertyChanged
{
public IndexPickerViewModel MyIndexPicker{ get; set; }
public string Name { get ; set; }
public int Id
{
get
{
return MyIndexPicker.Index;
}
set
{
if (value != MyIndexPicker.Index)
{
MyIndexPicker.Index = value;
NotifyPropertyChanged("Id");
}
}
}
public CustomControlViewModel(string _name)
{
Name = _name;
MyIndexPicker = new IndexPickerViewModel();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName);
}
}
}
IndexPickerView.xaml:
<UserControl x:Class="IndexPicker.IndexPickerView"
...
...>
<Grid>
<Combobox ItemsSource="{Binding Path=MyTable}"
DisplayMemberPath="ColumnXYZ"
SelectedItem={Binding Path=SelectedRow}/>
</Grid>
</UserControl>
Finally
IndexPickerViewModel.cs:
namespace IndexPicker
{
public class IndexPickerViewModel : INotifyPropertyChanged
{
private DataAccess data;
public DataView MyTable { get; set; }
private DataRowView selectedRow;
public DataRowView SelectedRow
{
get { return selectedRow; }
set
{
selectedRow = value;
NotifyPropertyChanged("SelectedRow");
}
}
public int? Index
{
get
{
if (SelectedRow != null) return (int?)selectedRow.Row["Column_Id"];
else return null;
}
set
{
SelectedRow = MyTable[MyTable.Find((int)value)];
NotifyPropertyChanged("Index");
}
}
public IndexPickerViewModel()
{
data = new DataAccess();
MyTable = data.GetTableView("tableName");
MyTable.Sort = "Column_Id";
}
// And don't forget INotifyPropertyChanged implementation
}
}
This configuration is used with several different UserControls inheriting from CustomControlView and their ViewModel inheriting from CustomControlViewModel. They are dynamically created and listed in CustomVMList. Here CustomControlViewModel containing an IndexPicker is already a specialization.
Concrete use: Generic Dialog for CRUD database Tables, which can dynamically create UserControls depending on each Table Columns. The specialization shown here is used in case of a column containing a foreign key.
I hope its clear.
The code listed above may contain mistakes. Criticisms and remarks are welcome.
I am trying to grasp the concepts of WPF data binding through a simple example, but it seems I haven't quite gotten the point of all of it.
The example is one of cascading dropdowns; the XAML is as follows:
<Window x:Class="CascadingDropDown.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="496" Width="949" Loaded="Window_Loaded">
<Grid>
<ComboBox Name="comboBox1" ItemsSource="{Binding}" DisplayMemberPath="Key" SelectionChanged="comboBox1_SelectionChanged" />
<ComboBox Name="comboBox2" ItemsSource="{Binding}" DisplayMemberPath="Name" />
</Grid>
</Window>
This is the code of the form:
public partial class MainWindow : Window
{
private ObservableCollection<ItemA> m_lstItemAContext = new ObservableCollection<ItemA>();
private ObservableCollection<ItemB> m_lstItemBContext = new ObservableCollection<ItemB>();
private IEnumerable<ItemB> m_lstAllItemB = null;
public MainWindow()
{
InitializeComponent();
this.comboBox1.DataContext = m_lstItemAContext;
this.comboBox2.DataContext = m_lstItemBContext;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var lstItemA = new List<ItemA>() { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
var lstItemB = new List<ItemB>() { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
initPicklists(lstItemA, lstItemB);
}
private void initPicklists(IEnumerable<ItemA> lstItemA, IEnumerable<ItemB> lstItemB)
{
this.m_lstAllItemB = lstItemB;
this.m_lstItemAContext.Clear();
lstItemA.ToList().ForEach(a => this.m_lstItemAContext.Add(a));
}
#region Control event handlers
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox ddlSender = (ComboBox)sender;
ItemA itemaSelected = (ItemA)ddlSender.SelectedItem;
var lstNewItemB = this.m_lstAllItemB.Where(b => b.KeyA == itemaSelected.Key);
this.m_lstItemBContext.Clear();
lstNewItemB.ToList().ForEach(b => this.m_lstItemBContext.Add(b));
}
private void comboBox2_?(object sender, ?EventArgs e)
{
// disable ComboBox if empty
}
#endregion Control event handlers
}
And these are my data classes:
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
}
class ItemB
{
public string KeyA { get; set; }
public string Name { get; set; }
public ItemB(string sKeyA, string sName)
{
this.KeyA = sKeyA;
this.Name = sName;
}
}
So whenever an item is selected in comboBox1, the appropriate items are supposed to show up in comboBox2. This is working with the current code, though I'm not sure whether my way of re-populating the respective ObservableCollection is ideal.
What I haven't been able to achieve is actually reacting to changes in the underlying collection of comboBox2, for example to deactivate the control when the list is empty (i.e. when "ccc" is selected in comboBox1).
Of course, I can use an event handler on the CollectionChanged event of the ObservableCollection, and that would work in this example, but in a more complex scenario, where the ComboBox' DataContext might change to a completely different object (and possibly back), that would mean a two-fold dependency - I would always have to not only switch the DataContext, but also the event handlers back and forth. This doesn't seem right to me, but I am probably simply on an entirely wrong track about this.
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items".
What do I need to do, or where do I have to correct my perception of the whole concept ?
Here is the cleaner (perhaps not the much optimized) way to acheive this, keeping your business model untouched, and using ViewModel and XAML only when possible :
View Model :
public class WindowViewModel : INotifyPropertyChanged
{
private ItemA selectedItem;
private readonly ObservableCollection<ItemA> itemsA = new ObservableCollection<ItemA>();
private readonly ObservableCollection<ItemB> itemsB = new ObservableCollection<ItemB>();
private readonly List<ItemB> internalItemsBList = new List<ItemB>();
public WindowViewModel()
{
itemsA = new ObservableCollection<ItemA> { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
InvokePropertyChanged(new PropertyChangedEventArgs("ItemsA"));
internalItemsBList = new List<ItemB> { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
}
public ObservableCollection<ItemA> ItemsA
{
get { return itemsA; }
}
public ItemA SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
ItemsB.Clear();
var tmp = internalItemsBList.Where(b => b.KeyA == selectedItem.Key);
foreach (var itemB in tmp)
{
ItemsB.Add(itemB);
}
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
}
}
public ObservableCollection<ItemB> ItemsB
{
get { return itemsB; }
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
Code Behind :
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new WindowViewModel();
}
}
and XAML :
<StackPanel>
<ComboBox Name="comboBox1" ItemsSource="{Binding ItemsA}" DisplayMemberPath="Key" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
<ComboBox Name="comboBox2" ItemsSource="{Binding ItemsB}" DisplayMemberPath="Name">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="IsEnabled" Value="true"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsB.Count}" Value="0">
<Setter Property="IsEnabled" Value="false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</StackPanel>
copying-pasting this should work.
Few random thoughts :
1) in WPF, try to always use MVVM pattern and never put code in code-behind files for event handlers. For user actions (like button clicks) use the Commands pattern. For other user actions for which commands are not available, think as much as you can in a "binding-way" : you can do a lot since you can intercept event from the view in VM properties setters (in your example I use the SelectedItem property setter).
2) Use XAML as much as you can. WPF framework provides a very powerful binding and triggers system (in your example, the enabling of combobox don't needs any line of C#).
3) ObservableCollection are made to be exposed by the view model to the view via binding. They are also meant to be used in conjunction with their CollectionChanged event that you can handle in the view model. Take benefit of that (in your example, I play with Observable collection in the VM, where this playing should happen, and any changes in the collection gets reflected in the view via DataBinding).
Hopes this will help !
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items"
if you wanna use MVVM pattern then i would say NO. not the control should give the information, but your viewmodel should.
taking an ObservableCollection is a good step at first. in your specail case i would consider to create just one list with ItemA and i would add a new List property of type ItemB to ItemA.
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
public IEnumerable<ItemB> ListItemsB { get; set;}
}
i assume ItemA is the parent?
class ItemB
{
public string Name { get; set; }
public ItemB(string sName)
{
this.Name = sName;
}
}
you have a collection of ItemA and each ItemA has its own list of depending ItemB.
<ComboBox x:Name="cbo_itemA" ItemsSource="{Binding ListItemA}" DisplayMemberPath="Key"/>
<ComboBox ItemsSource="{Binding ElementName=cbo_itemA, Path=SelectedItem.ListItemsB}"
DisplayMemberPath="Name" />
Do you need the Keys collection? If not i'd suggest creating it dynamically from the items by grouping via CollectionView:
private ObservableCollection<object> _Items = new ObservableCollection<object>()
{
new { Key = "a", Name = "Item 1" },
new { Key = "a", Name = "Item 2" },
new { Key = "b", Name = "Item 3" },
new { Key = "c", Name = "Item 4" },
};
public ObservableCollection<object> Items { get { return _Items; } }
<StackPanel>
<StackPanel.Resources>
<CollectionViewSource x:Key="ItemsSource" Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Key"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</StackPanel.Resources>
<StackPanel.Children>
<ComboBox Name="keyCb" ItemsSource="{Binding Source={StaticResource ItemsSource}, Path=Groups}" DisplayMemberPath="Name"/>
<ComboBox ItemsSource="{Binding ElementName=keyCb, Path=SelectedItem.Items}" DisplayMemberPath="Name"/>
</StackPanel.Children>
</StackPanel>
The first ComboBox shows the keys which are generated by grouping by the Key-property, the second binds to the selected item's subitems in the first ComboBox, showing the Name of the item.
Also see the CollectionViewGroup reference, in the fist CB i use the Name in the second the Items.
Of course you can create these key-groups manually as well by nesting items in a key-object.
I need to bind so that the Content of a content control is set to the SelectedValue of either the TreeView or the ListBox. The SelectedValue that was most recently changed should provide the content for the ContentControl.
I was able to get this working using the following concept.
Bind the content control to a read only property "SelectedItem" (with private property _selectedItem).
Bind the ListBox.SelectedItem to a read/write property "SelectedItemLB".
In the SelectedItemLB setter, set the value of _selectedItem, and raise the PropertyChanged event for SelectedItem.
Create a handler for VreeView.SelectedItemChanged, which sets the value of _selectedItem and raises the PropertyChanged event for SelectedItem.
Here is my full code:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.items = new List<object>();
this.items.Add(new Car("Green"));
this.items.Add(new Car("Blue"));
this.items.Add(new Car("Red"));
this._selectedItem = this.items[0];
this.treeView1.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(treeView1_SelectedItemChanged);
this.DataContext = this;
}
void treeView1_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
this._selectedItem = treeView1.SelectedItem;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
private List<object> items;
public List<object> Items
{
get { return items; }
set { items = value; }
}
public object SelectedItemLB
{
get { return _selectedItem; }
set
{
_selectedItem = value;
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
}
private object _selectedItem;
public object SelectedItem
{
get { return _selectedItem; }
}
public event PropertyChangedEventHandler PropertyChanged;
}
The XAML is pretty simple:
<StackPanel>
<ListBox Name="listBox1" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=SelectedItemLB, Mode=TwoWay}" ></ListBox>
<TreeView Name="treeView1" ItemsSource="{Binding Path=Items}">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected}"></Setter>
</Style>
</TreeView.Resources>
</TreeView>
<ContentControl Content="{Binding Path=SelectedItem.Color}"></ContentControl>
</StackPanel>
I can't think of a way to do that directly. However there are several straightforward solutions.
A. Use events to set the Content
Simply attach a common handler to the SelectedValueChanged events of your ItemsControls. Whenever one of them changes its selection, the handler will set the Content to whatever was selected. I think this is most simple.
B. Use intermediary properties
Bind the SelectedValue of each ItemsControl to a property. In the property's setter, also set the Content equal to value. This allows you to use data binding instead of event handlers, but it still requires you to write code-behind and it doesn't buy you much. Of course, if you are already binding to properties for other purposes, there is almost no extra cost (only an assignment in each setter) so this method might be preferable.
I am trying to produce a list of servers for browsing on a network such that it produces a tree view which looks like this:
-Local Server
- Endpoint 1
- Endpoint 2
-Remote
- <Double-click to add a server...>
- Remote Server 1
- Endpoint 1
- Endpoint 2
- Remote Server 2
- Endpoint 1
- Endpoint 2
My ViewModel looks like this:
...
public Server LocalServer;
public ObservableCollection<Server> RemoteServers;
...
So, how does one go about constructing the list in xaml with a binding to a single object and a list of objects? I might be thinking about it completely the wrong way, but what my brain really wants to be able to do is something like this:
<CompositeCollection>
<SingleElement Content="{Binding LocalServer}">
<!-- ^^ something along the lines of a ContentPresenter -->
<TreeViewItem Header="Remote">
<TreeViewItem.ItemsSource>
<CompositeCollection>
<TreeViewItem Header="<Click to add...>" />
<CollectionContainer Collection="{Binding RemoteServers}" />
</CompositeCollection>
</TreeViewItem.ItemsSource>
</TreeViewItem>
</CompositeCollection>
I feel like there must be a fundamental element I'm missing which keeps me from being able to specify what I want here. That single item has children. I did try using a ContentPresenter, but for whatever reason, it was not expandable even though it picked up the HierarchicalDataTemplate to display the title correctly.
Update
So for now, I've exposed a property on the view model that wraps the single element in a collection so that a CollectionContainer may bind to it. I would really like to hear folks' ideas on how to do this, though. It seems awfully fundamental.
I posted a question very similar to yours regarding CompositeCollections: Why is CompositeCollection not Freezable?
This is apparently a bug in WPF, believe it or not. Here's a post by an MS employee admitting as much: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b15cbd9d-95aa-47c6-8068-7ae9f7dca88a
The CompositeCollection is not freezable, but should be. This makes it difficult to combine nonstatic elements into one collection. It's a common scenario for a lot of things. For example, a "Select One" element at the top of a combobox filled with other databound objects would be nice, but you can't do it declaratively.
Anyway, I'm sorry this is not an answer, but hopefully it helps you see why this isn't working how you thought it should.
Can't you just expose a new collection from your ViewModel that the tree can bind to?
Something like:
public Server LocalServer;
public ObservableCollection<Server> RemoteServers;
public IEnumerable ServerTree { return new[] { LocalServer, RemoteServers } }
After all your ViewModel is a ViewModel. It should be exposing exactly what is needed by the view.
Finally, just after a few years, my WPF skills are good enough to solve this one ;)
Here's a SingleElement like you outlined in your question. It is implemented by subclassing a CollectionContainer and putting the bound element inside the collection. By registering a change handler we can even update the CollectionContainer when the binding changes. For the original CollectionProperty we specify a coercion handler to prevent users of our class to mess with the collection property, if you would like to improve the protection you could use a custom collection instead of an ObservableCollection. As a bonus I show how to make the SingleElement disappear by using a placeholder value, though technically that would be more of an "OptionalSingleElement".
public class SingleElement : CollectionContainer
{
public static readonly object EmptyContent = new object();
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
"Content", typeof(object), typeof(SingleElement), new FrameworkPropertyMetadata(EmptyContent, HandleContentChanged));
static SingleElement()
{
CollectionProperty.OverrideMetadata(typeof(SingleElement), new FrameworkPropertyMetadata { CoerceValueCallback = CoerceCollection });
}
private static object CoerceCollection(DependencyObject d, object baseValue)
{
return ((SingleElement)d)._content;
}
private static void HandleContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var content = ((SingleElement)d)._content;
if (e.OldValue == EmptyContent && e.NewValue != EmptyContent)
content.Add(e.NewValue);
else if (e.OldValue != EmptyContent && e.NewValue == EmptyContent)
content.RemoveAt(0);
else // (e.OldValue != EmptyContent && e.NewValue != EmptyContent)
content[0] = e.NewValue;
}
private ObservableCollection<object> _content;
public SingleElement()
{
_content = new ObservableCollection<object>();
CoerceValue(CollectionProperty);
}
public object Content
{
get { return GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
}
You can use it exactly like you stated it in your question, except that you have to adjust for the lack of a DataContext in the CompositeCollection:
<TreeView x:Name="wTree">
<TreeView.Resources>
<CompositeCollection x:Key="Items">
<local:SingleElement Content="{Binding DataContext.LocalServer, Source={x:Reference wTree}}"/>
<TreeViewItem Header="Remote">
<TreeViewItem.ItemsSource>
<CompositeCollection>
<TreeViewItem Header="<Click to add ...>"/>
<CollectionContainer Collection="{Binding DataContext.RemoteServers, Source={x:Reference wTree}}"/>
</CompositeCollection>
</TreeViewItem.ItemsSource>
</TreeViewItem>
</CompositeCollection>
</TreeView.Resources>
<TreeView.ItemsSource>
<StaticResource ResourceKey="Items"/>
</TreeView.ItemsSource>
</TreeView>
Well, this is the closest I can come to your requirements. All the functionality is not contained within one TreeView, nor is it bound to a compositecollection, but that can remain a secret between you and me;)
<Window x:Class="CompositeCollectionSpike.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
xmlns:local="clr-namespace:CompositeCollectionSpike">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="TreeView">
<Setter Property="BorderThickness" Value="0"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:Server}"
ItemsSource="{Binding EndPoints}">
<Label Content="{Binding Name}"/>
</HierarchicalDataTemplate>
</StackPanel.Resources>
<TreeView ItemsSource="{Binding LocalServer}"/>
<TreeViewItem DataContext="{Binding RemoteServers}"
Header="{Binding Description}">
<StackPanel>
<Button Click="Button_Click">Add Remote Server</Button>
<TreeView ItemsSource="{Binding}"/>
</StackPanel>
</TreeViewItem>
</StackPanel>
using System.Collections.ObjectModel;
using System.Windows;
namespace CompositeCollectionSpike
{
public partial class Window1 : Window
{
private ViewModel viewModel;
public Window1()
{
InitializeComponent();
viewModel = new ViewModel
{
LocalServer =new ServerCollection{new Server()},
RemoteServers =
new ServerCollection("Remote Servers") {new Server(),
new Server(), new Server()},
};
DataContext = viewModel;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
viewModel.LaunchAddRemoteServerDialog();
}
}
public class ViewModel:DependencyObject
{
public ServerCollection LocalServer { get; set; }
public ServerCollection RemoteServers { get; set; }
public void LaunchAddRemoteServerDialog()
{}
}
public class ServerCollection:ObservableCollection<Server>
{
public ServerCollection(){}
public ServerCollection(string description)
{
Description = description;
}
public string Description { get; set; }
}
public class Server
{
public static int EndpointCounter;
public static int ServerCounter;
public Server()
{
Name = "Server"+ ++ServerCounter;
EndPoints=new ObservableCollection<string>();
for (int i = 0; i < 2; i++)
{
EndPoints.Add("Endpoint"+ ++EndpointCounter);
}
}
public string Name { get; set; }
public ObservableCollection<string> EndPoints { get; set; }
}
}