WPF TreeView databinding to hide/show expand/collapse icon - wpf

I implemented a WPF load-on-demand treeview like described in this (very good) article.
In the mentioned solution a dummy element is used to preserve the expand + icon / treeview item behavior. The dummy item is replaced with real data, when the user clicks on the expander.
I want to refine the model by adding a property public bool HasChildren { get { ... } } to my backing TreeNodeViewModel.
Question:
How can I bind this property to hide/show the expand icon (in XAML)? I am not able to find a suitable trigger/setter combination.
(INotifyPropertyChanged is properly implemented.)
I want to use my property public bool HasChildren instead of using the dummy element.
Determining whether or not an item has children is somewhat costly, but still much cheaper than fetching the children.

Julian,
this is a really good question. Why don't you try to write your own tree view item? :) I mean, not from the scratch, just derive from existing TreeViewItem and add your property. I have prepared a quick sample, but feel free to modify it as you wish (and ask questions if something is not perfectly clear). here we go:
public class TreeViewItem_CustomControl : TreeViewItem
{
static TreeViewItem_CustomControl()
{
HasChildrenProperty = DependencyProperty.Register("HasChildren", typeof(Boolean), typeof(TreeViewItem_CustomControl));
}
static DependencyProperty HasChildrenProperty;
public Boolean HasChildren
{
get
{
return (Boolean)base.GetValue(HasChildrenProperty);
}
set
{
if (value)
{
if (this.Items != null)
{
this.Items.Add(String.Empty); //Dummy item
}
}
else
{
if (this.Items != null)
{
this.Items.Clear();
}
}
base.SetValue(HasChildrenProperty, value);
}
}
}
This was the code for your custom TreeViewItem. Now let's use it in XAML:
<TreeView>
<TreeViewItem Header="qwer">
Regulat tree view item.
</TreeViewItem>
<CustomTree:TreeViewItem_CustomControl x:Name="xyz" Header="temp header" Height="50">
<TreeViewItem>Custom tree view item, which will be removed.</TreeViewItem>
</CustomTree:TreeViewItem_CustomControl>
</TreeView>
As you can see, first item is a regular one and the second is your custom one. Please note, that it has one child. Next, you can bind HasChildren property to some Boolean object in your ViewModel or just simply test my custom class by setting HasChildren to False from code behind the above XAML:
xyz.HasChildren = false;
Now, despite your element has one child, expand button is not displayed, so it means, that my custom class works.
I hope I helped you, but feel free to ask if you have any questions.
Piotr.

After quickly looking into Josh's code, I found this constructor:
protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
{
_parent = parent;
_children = new ObservableCollection<TreeViewItemViewModel>();
if (lazyLoadChildren)
_children.Add(DummyChild);
}
So, if you pass false for the lazyLoadChildren parameter from your inheriting ViewModel classes, the + icon should not appear because the DummyChild is not added. Since you seem to know whether your items have children or not, you should be able to pass the appropriate value for the lazyLoadChildren property. Or am I missing something?

Related

Is it possible to create a control once and have it generated everytime it is needed?

Say for example you have a stackpanel that you would like to programmatically add buttons to.
The code behind for the button generation and addition to the stackpanel is:
Button button = new Button();
button.Content = "Button";
button.HorizontalAlignment = HorizontalAlignment.Left;
button.Name = "Button" + i
stackPanel1.Children.Add(button);
My question is - Is it possible to generate the button once and have it as some kind of template that can be added to the stackpanel whenever it is needed without going through the generation code again?
In WPF each UIElement can only be the logical child of one control at any given time, see WPF Error: Specified element is already the logical child of another element. Disconnect it first, so no you can't use the same button and add it to another control later on, unless you're sure you've gotten rid of that stackpanel
But, you can do recycling. See Optimizing Performance: Controls. especially if you are willing to override MeasureOverride and ArrangeOverride of you stackpanel
I've actually written such a recycler because I had grids with many controls, and I wanted to implement some kind of virtualizing grid. These are the main methods of my class:
internal class Recycler
{
private UIElementCollection _children;
public Recycler(UIElementCollection children)
{
_children = children;
//You need this because you're not going to remove any
//UIElement from your control without calling this class
}
public void Recycle(UIElement uie)
{
//Keep this element for recycling, remove it from Children
}
public UIElement GiveMeAnElement(Type type)
{
//Return one of the elements you kept of this type, or
//if none remain create one using the default constructor
}
}

User controls communicating via Commands - how?

I'm working on my first project in WPF/XAML, and there's a lot I've not figured out.
My problem is simple - I need a window that has a bunch of fields at the top, with which the user will enter his selection criteria, a retrieve button, and a data grid. When the user clicks on the button, a query is run, and the results are used to populate the grid.
Now the simple and obvious and wrong way to implement this is to have a single module containing a single window, and have everything contained within it - entry fields, data grid, the works. That kind of mangling of responsibilities makes for an unmaintainable mess.
So what I have is a window that is responsible for little more than layout, that contains two user controls - a criteria control that contains the entry fields and the retrieve button, and a data display control that contains the data grid.
The question is how to get the two talking to each other.
Years back, I would have added a function pointer to the criteria control. The window would have set it to point to a function in the display control, and when the button was clicked, it would have called into the display control, passing the selection criteria.
More recently, I would have added an event to the criteria control. I would have had the window set a handler in the display control to listen to the event, and when the button was clicked, it would have raised the event.
Both of these mechanisms would work, in WPF. But neither is very XAMLish. It looks to me like WPF has provided the ICommand interface specifically to accommodate these kinds of connection issues, but I've not yet really figured out how they are intended to work. And none of the examples I've seen seem to fit my simple scenario.
Can anyone give me some advice on how to fit ICommand to this problem? Or direct me to a decent explanation online?
Thanks!
MVVM is the prevalent pattern used with WPF and Silverlight development. You should have a read up on it.
Essentially, you would have a view model that exposes a command to perform the search. That same view model would also expose properties for each of your criteria fields. The view(s) would then bind to the various properties on the view model:
<TextBox Text="{Binding NameCriteria}"/>
...
<Button Command="{Binding SearchCommand}".../>
...
<DataGrid ItemsSource="{Binding Results}"/>
Where your view model would look something like:
public class MyViewModel : ViewModel
{
private readonly ICommand searchCommand;
private string nameCriteria;
public MyViewModel()
{
this.searchCommand = new DelegateCommand(this.OnSearch, this.CanSearch);
}
public ICommand SearchCommand
{
get { return this.searchCommand; }
}
public string NameCriteria
{
get { return this.nameCriteria; }
set
{
if (this.nameCriteria != value)
{
this.nameCriteria = value;
this.OnPropertyChanged(() => this.NameCriteria);
}
}
}
private void OnSearch()
{
// search logic, do in background with BackgroundWorker or TPL, then set Results property when done (omitted for brevity)
}
private bool CanSearch()
{
// whatever pre-conditions to searching you want here
return !string.IsEmpty(this.NameCriteria);
}
}

Collapse all groups except the first one

I have a DataGrid with grouped ItemsSource. There are an expander on each group, so I can expand/collapse all the groups. Now, I'm trying to collapse all groups by default, but leave the first group expanded. The items source is dynamic, so I can't build any converter to check the group name. I must do it by group index.
Is it possible to do in in XAML? Or in code-behind?
This might be a little late, but in order to help with similar problems, defining a "Visual tree helper class" would be helpful in this case.
// the visual tree helper class
public static class VisualTreeHelper
{
public static Collection<T> GetVisualChildren<T>(DependencyObject current) where T : DependencyObject
{
if (current == null)
return null;
var children = new Collection<T>();
GetVisualChildren(current, children);
return children;
}
private static void GetVisualChildren<T>(DependencyObject current, Collection<T> children) where T : DependencyObject
{
if (current != null)
{
if (current.GetType() == typeof(T))
children.Add((T)current);
for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(current); i++)
{
GetVisualChildren(System.Windows.Media.VisualTreeHelper.GetChild(current, i), children);
}
}
}
}
// then you can use the above class like this:
Collection<Expander> collection = VisualTreeHelper.GetVisualChildren<Expander>(dataGrid1);
foreach (Expander expander in collection)
expander.IsExpanded = false;
collection[0].IsExpanded = true;
the credit goes to this forum
I was able to solve this in my ViewModel.
The Expander is defined in the template of the DataGrids GroupStyle. The Binding must be TwoWay but triggered explicitly, so clicking in the View does not update the ViewModel. Thanks Rachel.
<Expander IsExpanded="{Binding DataContext.AreAllGroupsExpanded, RelativeSource={RelativeSource AncestorType={x:Type local:MyControl}}, UpdateSourceTrigger=Explicit}">
...
</Expander>
Then I can just set the property AreAllGroupsExpanded in my ViewModel.
I don't believe it can be done in the XAML, but it can be done in code-behind. Here is one solution that I tested in Silverlight. It should probably work just as well in WPF.
// If you don't have a direct reference to the grid's ItemsSource,
// then cast the grid's ItemSource to the type of the source.
// In this example, I used a PagedCollectionView for the source.
PagedCollectionView pcv = (PagedCollectionView)myDataGrid.ItemsSource;
// Using the PagedCollectionView, I can get a reference to the first group.
CollectionViewGroup firstGroup = (CollectionViewGroup)pcv.Groups[0];
// First collapse all groups (if they aren't already collapsed).
foreach (CollectionViewGroup group in pcv.Groups)
{
myDataGrid.ScrollIntoView(group, null); // This line is a workaround for a problem with collapsing groups when they aren't visible.
myDataGrid.CollapseRowGroup(group, true);
}
// Now expand only the first group.
// If using multiple levels of grouping, setting 2nd parameter to "true" will expand all subgroups under the first group.
myDataGrid.ExpandRowGroup(firstGroup, false);
// Scroll to the top, ready for the user to see!
myDataGrid.ScrollIntoView(firstGroup, null);

DataContext for binding a UserControl with ViewModel within DataTemplate

What I'm trying to achieve is:
Have a ListView bound to an ObservableCollection of ItemRecords.
Have a TabControl that contains detailed view for all the ItemRecords in the ListView that were selected for editing.
Each TabItem contains a UserControl ("ItemInfo") that uses ItemInfoViewModel as its VM (and, not so coincidentally, DataContext).
ItemInfo UserControl needs to be populated with the data from the corresponding ItemRecord.
To achieve that, I'm trying to pass the ItemRecord (selected in the ListView) to ItemInfoViewModel.
Finally, the question: what do you think would be the best way to do this, without breaking the MVVM pattern?
The not-so-elegant way that I see (and it actually doesn't exactly follow the MVVM principles) is to have a DependencyProperty ItemRecord defined in the UserControl, provide its value via binding, and in the constructor (in the UserControl's code-behind) pass the ItemRecord to the VM (which we get by casting the DataContext).
The other problem is with how to actually pass the ItemRecord via binding.
Once I set the VM as the UserControl's DataContext, I cannot just use {Binding} to specify the current item in TabControl's source collection.
At the moment I am binding to the TabControl's SelectedItem using ElementName - but this doesn't sound too robust :-(
<localControls:TabControl.ContentTemplate>
<DataTemplate>
<ScrollViewer>
<localControls:ItemInfo ItemRecord="{Binding ElementName=Tabs, Path=SelectedItem}"/>
</ScrollViewer>
</DataTemplate>
</localControls:TabControl.ContentTemplate>
Any good advice will be greatly appreciated!
Alex
I think your problem is you're not quite understanding the MVVM pattern here; you're still looking at this as the different controls talking to each other. Where in MVVM, they should not be, each control is communicating with the view model independently of all the others. And the view model controls (and supplies) the logic which tells the controls how to behave.
So, ideally you would have something like:
public ObservableCollection<ItemRecord> ListViewRecords
{
get { ... }
set { ... }
}
public IEnumerable<ItemRecord> SelectedListViewRecords {
{
get { ... }
set { ... }
}
The ListViewRecords would be bound to the ItemsSource property of your ListView (the actual properties might vary based on the specific controls you're using, I'm used to the Telerik suite at the moment so that's where my head is). And the SelectedListViewRecords would be bound to the SelectedItems property of the ListView. Then for your TabControl you would have:
public ObservableCollection<MyTabItem> Tabs
{
get { ... }
set { ... }
}
public MyTabItem SelectedTab
{
get { ... }
set { ... }
}
Again, you would bind the Items property to the Tabs and SelectedItem to the SelectedTab on your TabControl. Now your view model contains all the logic, so in your SelectedListViewRecords you might do something like this:
public IEnumerable<ItemRecord> SelectedListViewRecords {
{
get { ... }
set
{
_selectedRecords = value;
NotifyPropertyChanged("SelectedListViewRecords");
Tabs.Clear(); // Clear the existing tabs
// Create a new tab for each newly selected record
foreach(ItemRecord record in value)
Tabs.Add(new MyTabItem(record));
}
}
So the idea here is that the controls do nothing more than send and receive property changes, they know nothing of the underlying data, logic, etc. They simply show what their bound properties tell them to show.

Access DataGridCell's children from another DataGridCell?

I have a DataGridCell that contains a ComboBox.
I want, that when I fire 'SelectionChanged' event of it, a CollectionViewSource of a different column (eventually - at runtime, cell) CellEditingTemplate's Resources should be populated with data according to the selected value for this row.
Maybe DataTrigger, ActionTrigger, EventTrigger, maybe by code, XAML I don't care, I just need a solution.
Thanks a lot!
Related: Accessing control between
DataGridCells, dynamic cascading
ComboBoxes
If I understand your question right, you will fill the contents of a combobox in a cell based on the selection of a combobox in another cell that is in the same row of the DataGrid.
If yes:
First Solution (IMO the preferable)
Make a ViewModel that represents the rows data (a simple wrapper around your data object). Bind the ItemsSource-property of the destination ComboBox to a IEnumerable-property that you provide from your viewmodel.
Bind the SelectedItem from the source-ComboBox to another property of your ViewModel. Every time this source-property changes in your ViewModel, you change the contents of the list that is provided by the ViewModel.
Use for the desintation (list) property a ObservableCollection<T>. The source property is up to you.
Here is an approximately example. I call the class VM (for ViewModel) but this changes nothing on your current solution. MVVM can also be used partial.
public class DataObjectVM : DependencyObject {
public static readonly DependencyProperty SelectedCategoryProperty =
DependencyProperty.Register("SelectedCategory", typeof(CategoryClass), typeof(DataObjectVM), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,delegate (DependencyObject d,DependencyPropertyChangedEventArgs e){
((DataObjectVM)d).SelectedCategoryChanged(e);
}));
ObservableCollection<ItemClass> _items=new ObservableCollection<ItemClass>();
void SelectedCategoryChanged(DependencyPropertyChangedEventArgs e) {
// Change here the contents of the _items collection.
// The destination ComboBox will update as you desire
// Do not change the _items reference. Only clear, add, remove or
// rearange the collection-items
}
// Bind the destination ComboxBox.ItemsSource to this property
public IEnumerable<ItemClass> DestinationItems {
get {
return _items;
}
}
// Bind to this property with the source ComboBox.SelectedItem
public CategoryClass SelectedCategory {
get { return (CategoryClass)GetValue(SelectedCategoryProperty); }
set { SetValue(SelectedCategoryProperty, value); }
}
}
Add a constructor to this class that takes your data object and make some wrapper properties to the rest the properties you need to provide in the DataGrid. If they are alot, you can also make one property that provides your data object and the bind directly to it. Not nice, but it will do the job.
You also can (must) pre-initialize the SelectedCategory with data from your business object. Do this also in the constructor.
As a ItemsSource for the DataGrid you give an IEnumerable of the DataObjectVM-class that wrapps all items you want to show.
Alternative way with VisualTreeHelper
If you want to do it manual, register in the code behind a handler for the ComboBox.SelectionChangedEvent and change then the ItemsSource of the destination ComboBox manual. The business-object you will get with the EventArgs. The destination ComboBox you must search in the visual tree (Use the VisualTreeHelper). The events can be wired also if you use the DataGridTemplateColumn class and add a DataTemplate with the corresponding ComboBoxes.
But I think this is realy not very simple to do and can be error prone. The above solution is much easier.
Here is the code you propably are looking for:
private void CboSource_SelectionChanged(object sender, SelectionChangedEventArgs e) {
ComboBox cbo = (ComboBox)sender;
FrameworkElement currentFe = VisualTreeHelper.GetParent(cbo) as FrameworkElement;
while (null != currentFe && !(currentFe is DataGridRow)) {
currentFe = VisualTreeHelper.GetParent(currentFe) as FrameworkElement;
}
if (null != currentFe) {
List<ComboBox> list = new List<ComboBox>();
FindChildFrameworkElementsOfType<ComboBox>(currentFe,list);
// Requirement 1: Find ComboBox
foreach (ComboBox cboFound in list) {
if (cboFound.Name == "PART_CboDestination") {
// This is the desired ComboBox
// Your BO is available through cbo.Found.DataContext property
// If don't like to check the name, you can also depend on the
// sequence of the cbo's because I search them in a deep search
// operation. The sequence will be fix.
}
}
List<DataGridCell> cells = new List<DataGridCell>();
FindChildFrameworkElementsOfType<DataGridCell>(currentFe,cells);
// Requirement 2: Find Sibling Cell
foreach (DataGridCell cell in cells) {
// Here you have the desired cell of the other post
// Take the sibling you are interested in
// The sequence is as you expect it
DataGridTemplateColumn col=cell.Column as DataGridTemplateColumn;
DataTemplate template = col.CellTemplate;
// Through template.Resources you can access the CollectionViewSources
// if they are placed in the CellTemplate.
// Change this code if you will have an edit cell template or another
// another construction
}
}
}
void FindChildFrameworkElementsOfType<T>(DependencyObject parent,IList<T> list) where T: FrameworkElement{
DependencyObject child;
for(int i=0;i< VisualTreeHelper.GetChildrenCount(parent);i++){
child = VisualTreeHelper.GetChild(parent, i);
if (child is T) {
list.Add((T)child);
}
FindChildFrameworkElementsOfType<T>(child,list);
}
}
And this is the markup I used:
<DataGrid.Columns>
<DataGridTemplateColumn Header="Source" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="PART_CboSource" SelectionChanged="CboSource_SelectionChanged" ItemsSource="!!YOUR ITEMS SOURCE!!" SelectedItem="{Binding Category}">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Destination">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="PART_CboDestination"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
Accessing the CollectionViewSource
To access the CollectionViewSource, put it into the resources section of the corresponding DataTemplate, not of the panel, then you will have direct access to them. IMO is this location anyway more appropriate than the resources-container of the grid.
If you dont't want to do this, check the state of the following post:
How to get logical tree of a DataTemplate

Resources