How to use ReactiveUI with a Hierarchical Data Source (Tree view) - c#

I've figured out a way to bind user controls inside a tree view dynamically with ReactiveUI.
But ...
The top level binding to the HierachicalDataSource is in the XAML not the code behind, and I need to set the ItemsSource directly and not use this.OneWayBind per the general pattern for ReactiveUI binding.
So, my question is: did I miss something in the ReactiveUI framework that would let me bind with this.OneWayBind and move the HierachicalDataTemplete into the code behind or a custom user control?
In particular- Is there another overload of OneWayBind supporting Hierarchical Data Templates, or a way to suppress the data template generation for the call when using it?
Update
I've added selected item, and programatic support for Expand and Selected to my test project, but I had to add a style to the XAML. I'd like to replace that with a simple RxUI Bind as well. Updated the examples.
Here are the key details:
Tree Control in Main View
<TreeView Name="FamilyTree" >
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:TreeItem}" ItemsSource="{Binding Children}">
<reactiveUi:ViewModelViewHost ViewModel="{Binding ViewModel}"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
main view code behind
public partial class MainWindow : Window, IViewFor<MainVM>
{
public MainWindow()
{
InitializeComponent();
//build viewmodel
ViewModel = new MainVM();
//Register views
Locator.CurrentMutable.Register(() => new PersonView(), typeof(IViewFor<Person>));
Locator.CurrentMutable.Register(() => new PetView(), typeof(IViewFor<Pet>));
//NB. ! Do not use 'this.OneWayBind ... ' for the top level binding to the tree view
//this.OneWayBind(ViewModel, vm => vm.Family, v => v.FamilyTree.ItemsSource);
FamilyTree.ItemsSource = ViewModel.Family;
}
...
}
MainViewModel
public class MainVM : ReactiveObject
{
public MainVM()
{
var bobbyJoe = new Person("Bobby Joe", new[] { new Pet("Fluffy") });
var bob = new Person("Bob", new[] { bobbyJoe });
var littleJoe = new Person("Little Joe");
var joe = new Person("Joe", new[] { littleJoe });
Family = new ReactiveList<TreeItem> { bob, joe };
_addPerson = ReactiveCommand.Create();
_addPerson.Subscribe(_ =>
{
if (SelectedItem == null) return;
var p = new Person(NewName);
SelectedItem.AddChild(p);
p.IsSelected = true;
p.ExpandPath();
});
}
public ReactiveList<TreeItem> Family { get; }
...
}
TreeItem base class
public abstract class TreeItem : ReactiveObject
{
private readonly Type _viewModelType;
bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
}
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set { this.RaiseAndSetIfChanged(ref _isSelected, value); }
}
private TreeItem _parent;
protected TreeItem(IEnumerable<TreeItem> children = null)
{
Children = new ReactiveList<TreeItem>();
if (children == null) return;
foreach (var child in children)
{
AddChild(child);
}
}
public abstract object ViewModel { get; }
public ReactiveList<TreeItem> Children { get; }
public void AddChild(TreeItem child)
{
child._parent = this;
Children.Add(child);
}
public void ExpandPath()
{
IsExpanded = true;
_parent?.ExpandPath();
}
public void CollapsePath()
{
IsExpanded = false;
_parent?.CollapsePath();
}
}
Person Class
public class Person : TreeItem
{
public string Name { get; set; }
public Person(string name, IEnumerable<TreeItem> children = null)
: base(children)
{
Name = name;
}
public override object ViewModel => this;
}
person view user control
<UserControl x:Class="TreeViewInheritedItem.PersonView"... >
<StackPanel>
<TextBlock Name="PersonName"/>
</StackPanel>
</UserControl>
Person view code behind
public partial class PersonView : UserControl, IViewFor<Person>
{
public PersonView()
{
InitializeComponent();
this.OneWayBind(ViewModel, vm => vm.Name, v => v.PersonName.Text);
}
...
}
Pet work the same as person.
And the full project is here ReactiveUI Tree view Sample

I reviewed the ReactiveUI source and this is the only way to do this. The Bind helper methods always use a DataTemplate and not a HierarchicalDataTemplate.
So this approach will use the XAML binding for the very top level and then let you use ReactiveUI binding on all of the TreeView items.
I'll see about creating a pull request to handle this edge case.
Thanks,
Chris

Related

ItemTemplate for UserControls in ItemsControl - WPF

My task is to implement a MDI-like interface in our WPF app.
I have created this simple class as a base for all the views:
public class BaseView : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private ViewType _type = ViewType.Null;
private string _tabTitle = string.Empty;
private bool _isSelected = false;
public ViewType Type { get => _type; set { _type = value; OnPropertyChanged(); } }
public string TabTitle { get => _tabTitle; set { _tabTitle = value; OnPropertyChanged(); } }
public bool IsSelected { get => _isSelected; set { _isSelected = value; OnPropertyChanged(); } }
}
Next, I created few test Views. All of them start like this:
<local:BaseView...
In main window, there are two controls: ItemsControl (for displaying the list of opened views), and ContentControl (for displaying the selected view.)
I store all the opened views in a ObservableCollection: ObservableCollection<BaseView>....
I wanted to display them as a list, so I created ItemsControl:
<ItemsControl x:Name="mainItemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Padding="2" Margin="2" Tag="{Binding Type}">
<TextBlock Text="{Binding TabTitle}" Foreground="White"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
When I set the ItemsControl's source (mainItemsControl.ItemsSource = openedViews;) and started the application, ItemsControl displayed the content of each View instead of the ItemTemplate (Border with the TextBlock). What did I do wrong?
If I understood correctly, then the openedViews collection consists of BaseView.
If so, then BaseView is a UIElement.
But Data Templates are used to render non UIElements.
If the Content receives a UIElement, then it is rendered directly as is.
One possible variant solution.
You need to remove the INotifyPropertyChanged interface from BaseView.
Create a data source for your BaseView with an implementation of INotifyPropertyChanged.
In BaseView create DependencyProperty for this source.
Create a simple, helper container for the openedViews collection.
Something like this (pseudo code):
public class SomeContainer
{
public BaseDataSource DataSource
{
get => _dataSource;
set
{
_dataSource = null;
if(View is not null)
{
View.DataSource = DataSource;
}
}
}
public BaseView View
{
get => _view;
set
{
_view = value;
if(_view is not null)
{
_view.DataSource = DataSource;
}
}
}
}

WPF: Context based header name change does not work in Tabcontrol, instead it changes for all tabs when used with templates

I have a tab control with Item header and Content template selectors
The tab header has a header at the end as "Add new tab". The content of the tab is a usercontrol called Ladder with a set of controls. templates are binded with viewmodels and all is fine.
<Grid.Resources>
<DataTemplate x:Key="newTabButtonContentTemplate"/>
<DataTemplate x:Key="newTabButtonHeaderTemplate">
<Button Content="Add New Tab" Command="{Binding ElementName=parentUserControl, Path=DataContext.NewCommand}"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
</Button>
</DataTemplate>
<DataTemplate x:Key="itemContentTemplate" DataType="{x:Type localNS:ItemViewModel}">
<views:Ladder DataContext="{Binding ItemLadderViewModel}"/>
</DataTemplate>
<DataTemplate x:Key="itemHeaderTemplate" DataType="{x:Type localNS:ItemViewModel}">
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
<templates:TemplateSelector x:Key="headerTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonHeaderTemplate}"
ItemTemplate="{StaticResource itemHeaderTemplate}"/>
<templates:TemplateSelector x:Key="contentTemplateSelector"
NewButtonTemplate="{StaticResource newTabButtonContentTemplate}"
ItemTemplate="{StaticResource itemContentTemplate}"/>
</Grid.Resources>
ViewModels
MainWindowViewModel
public MainWindowViewModel()
{
this.newCommand = new CustomDelegateCommand(
() =>
{
var itemViewModel = new ItemViewModel();
Items.Add(itemViewModel);
this.CurrentSelectedItem = itemViewModel;
}
);
}
private ObservableCollection<ItemViewModel> items;
public ObservableCollection<ItemViewModel> Items
{
get
{
if (items == null)
{
items = new ObservableCollection<ItemViewModel>();
var itemViewModel = new ItemViewModel();
Items.Add(itemViewModel);
this.CurrentSelectedItem = itemViewModel;
var itemsView = (IEditableCollectionView)CollectionViewSource.GetDefaultView(items);
itemsView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
}
return items;
}
}
public ItemViewModel CurrentSelectedItem
{
get { return this.currentSelectedItem; }
set
{
this.currentSelectedItem = value;
this.OnPropertyChanged();
}
}
ItemViewModel
public class ItemViewModel : BindableBase
{
private string header = "New Tab";
private LadderViewModel itemLadderViewModel;
public ItemViewModel()
{
itemLadderViewModel = new LadderViewModel();
Messenger.Default.Register<Instrument>(this, "InstrumentChanged", instrument =>
{
this.Header = instrument.FullName;
});
}
public LadderViewModel ItemLadderViewModel
{
get { return itemLadderViewModel; }
}
public string Header
{
get { return header; }
set
{
header = value;
OnPropertyChanged();
}
}
}
the usercontrol has a combobox with entries in that, if any entry is selected the tab header should be changed with that selected entry. I am using gala messenger to pass the message between objects, so when combobox is selected message is sent and the header is changed accordingly, this is also working but
The problem is: it changes the header of all tabs, I want the header of the tab to be changed only with the combobox item selected in that tab.
public class LadderViewModel : BindableBase
{
private Instrument ladderInstrumentName = null;
public Instrument LadderInstrumentName
{
get
{
return this.ladderInstrumentName;
}
set
{
this.ladderInstrumentName = value;
Messenger.Default.Send<Instrument>(value, "InstrumentChanged");
}
}
}
I want to know if this is the right approach of doing it and if yes what is the issue and if not then what is the right approach.
Thanks
MainWindowViewModel can listen messenger and change header only for selected tab:
public MainWindowViewModel()
{
this.newCommand = new CustomDelegateCommand(
() =>
{
var itemViewModel = new ItemViewModel();
Items.Add(itemViewModel);
this.CurrentSelectedItem = itemViewModel;
}
);
Messenger.Default.Register<Instrument>(this, "InstrumentChanged", instrument =>
{
if (CurrentSelectedItem != null) CurrentSelectedItem.Header = instrument.FullName;
});
}
ItemViewModel doesn't have subscription:
public ItemViewModel()
{
itemLadderViewModel = new LadderViewModel();
/*Messenger.Default.Register<Instrument>(this, "InstrumentChanged", instrument =>
{
this.Header = instrument.FullName;
});*/
}

Filtering ObservableCollection with ICollectionView

I have ObservableCollection binded to dataGrid and now I want to filter the presented data I see that I need to use ICollectionView but I am not sure how to add ICollectionView with my MVVM pattern.
My code simplified looks following:
public class MainViewModel : ViewModelBase , IBarcodeHandler
{
public ObservableCollection<TraceDataItem> TraceItemCollectionViewSource { get; set; }
}
My XAML
<Window xmlns:controls="clr-namespace:Mentor.Valor.vManage.RepairStation.Controls"
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"
<DataGrid Grid.Row="2" ColumnWidth="*" ItemsSource="{Binding TraceItemCollectionViewSource , Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" RowStyle="{StaticResource TraceRowStyle}" IsReadOnly="True" Name="TraceDataGrid" Margin="5,5,5,5" Padding="5,5,5,5" AutoGenerateColumns="False">
</Window>
How I can add ICollectionView here in order to apply filtering to the view?
You would need to:
public class MainViewModel : ViewModelBase, IBarcodeHandler
{
public ICollectionView TraceItemCollectionView
{
get { return CollectionViewSource.GetDefaultView(TraceItemCollectionViewSource); }
}
public ObservableCollection<TraceDataItem> TraceItemCollectionViewSource { get; set; }
}
then, somewhere in the code (maybe in the constructor) add your filter:
TraceItemCollectionView.Filter = o =>
{
var item = (TraceDataItem) o;
//based on item, return true if it should be visible, or false if not
return true;
};
And, in XAML, you would need to change the binding to TraceItemCollectionView property.
You may invoke the Filter callback from a Command and expose the View property from CollectionViewSource :
public class ViewModel: INotifyPropertyChanged
{
private CollectionViewSource data = new CollectionViewSource();
private ObservableCollection<Child> observableChilds = new ObservableCollection<Child>();
public ViewModel()
{
var model = new Model();
model.ChildList.Add(new Child { Name = "Child 1" });
model.ChildList.Add(new Child { Name = "Child 2" });
model.ChildList.Add(new Child { Name = "Child 3" });
model.ChildList.Add(new Child { Name = "Child 4" });
//Populate ObservableCollection
model.ChildList.ToList().ForEach(child => observableChilds.Add(child));
this.data.Source = observableChilds;
ApplyFilterCommand = new DelegateCommand(OnApplyFilterCommand);
}
public ICollectionView ChildCollection
{
get { return data.View; }
}
public DelegateCommand ApplyFilterCommand { get; set; }
private void OnApplyFilterCommand()
{
data.View.Filter = new Predicate<object>(x => ((Child)x).Name == "Child 1");
OnPropertyChanged("ChildCollection");
}
}
//Sample Model used
public class Model
{
public Model()
{
ChildList = new HashSet<Child>();
}
public ICollection<Child> ChildList { get; set; }
}
public class Child
{
public string Name { get; set; }
}
//View
<ListBox ItemsSource="{Binding Path = ChildCollection}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Command="{Binding ApplyFilterCommand}"/>
A CollectionView is not always the best solution. you can also filter your collection using some simple LinQ. Take this simple example:
public ObservableCollection<TraceDataItem> FilteredData
{
get
{
return new ObservableCollection<TraceDataItem>(YourUnfilteredCollection.Where(
i => MeetsFilterRequirements(i)));
}
}
private bool MeetsFilterRequirements(TraceDataItem item)
{
return item.SomeProperty == someValue || item is SomeType;
}
The beauty of this method is that you can add some complex filtering requirements. One thing to note: whenever any properties in this method are changed, you'd need to call NotifyPropertyChanged("FilteredData") to ensure that the UI will be updated accordingly.

Changing Content Type Questions

This question has been answered a few times but I can't seem to put the solution together. What I have below is what I'm glued together through various forums. I'm also new to WPF. I'm trying to switch the content of the MainWindow.xaml based some parameters. What I have:
<Window.Resources>
<DataTemplate x:Key="LogsGriDataTemplate" DataType="{x:Type viewModel:ViewModel1}">
<Label>This is a log</Label>
</DataTemplate>
<DataTemplate x:Key="ReportsGridDataTemplate" DataType="{x:Type viewModel:ViewModel2}">
<Label>This is a report</Label>
</DataTemplate>
</Window.Resources>
<ContentControl Grid.Row="1" Grid.Column="0" Content="{Binding CurrentPageViewModel}" />
private ViewModel1 _viewModel1 = new ViewModel1();
private ViewModel2 _viewModel2 = new ViewModel2();
private DataTemplate _CurrentPageViewModel;
public DataTemplate CurrentPageViewModel
{
get { return _CurrentPageViewModel; }
set { Set(() => CurrentPageViewModel, ref _CurrentPageViewModel, value); }
}
public void OnButtonPressMethod(object param)
{
if (view == 0)
{
CurrentPageViewModel = _viewModel1;
}
else
{
CurrentPageViewModel = _viewModel1;
}
}
The compiler is complaining about the CurrentPageViewModel = _viewModel1/2 statement saying you cannot set type ViewModel to type DataTemplate which makes sense. What should the CurrentPageViewModel property be? Is there anything else wrong with this code? Thanks.
The binding source should be your view model, not a DataTemplate. The DataTemplate with DataType definition in XAML will automatically bind content to the data template that matches the type.
So you could create a common interface/base class for view models 1 & 2:
public interface IViewModel { }
public class ViewModel1 : IViewModel { }
public class ViewModel2 : IViewModel { }
private IViewModel _viewModel1 = new ViewModel1();
private IViewModel _viewModel2 = new ViewModel2();
private IViewModel _CurrentPageViewModel;
public IViewModel CurrentPageViewModel
{
get { return _CurrentPageViewModel; }
set { Set(() => CurrentPageViewModel, ref _CurrentPageViewModel, value); }
}

Silverlight treeview: save expanded/collapsed state

I'm using a hierarchical tree view in Silverlight 4. This tree can be cleared and rebuilded quite often, depending on the user's actions. When this happends, the tree is collapsed by default, which can be annoying from a user perspective.
So, I want to somehow save which nodes are expanded so I can restore the visual state of my tree after it is clear and reloaded.
My treeview is implemented like this:
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:controls2="clr-namespace:System.Windows;assembly=System.Windows.Controls"
<controls:TreeView x:Name="Tree"
ItemsSource="{Binding Source={StaticResource ViewModel}, Path=TreeStructure, Mode=OneWay}"
ItemTemplate="{StaticResource hierarchicalTemplate}" />
<controls2:HierarchicalDataTemplate x:Key="hierarchicalTemplate" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Value.DisplayName}">
</controls2:HierarchicalDataTemplate>
ItemsSource of my treeview is bound on an ObservableCollection TreeStructure;
Node is a wrapper class that looks like that:
public class Node
{
public object Value { get; private set; }
public ObservableCollection<Node> Children { get; private set; }
public Node(object value)
{
Value = value;
Children = new ObservableCollection<Node>();
}
}
Pretty standard stuff. I saw some solutions for WPF, but I can find anything for the Silverlight tree view...
Any suggestions?
Thanks!
Given the way you are implementing your data as a tree, why not bind the 'TreeViewItem.IsExpanded` dependency property to a bool property on your own Node?
It will need to be an INotifyPropertyChanged property at a minimum so Node will need to implement INotifyPropertyChanged.
In Silverlight 5 you can just set a style like this to bind to the IsExpanded property:
<Style TargetType="sdk:TreeViewItem" x:Key="itemStyle">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
And use with
ItemContainerStyle="{Binding Source={StaticResource itemStyle}}"
In Silverlight 4 there are a number of workarounds.
Here's what I did to bind on the TreeViewItem.IsExpanded property. First, I added an IsExpanded property in my Node class.
public class Node : INotifyPropertyChanged
{
public object Value { get; private set; }
public ObservableCollection<Node> Children { get; private set; }
private bool isExpanded;
public bool IsExpanded
{
get
{
return this.isExpanded;
}
set
{
if (this.isExpanded != value)
{
this.isExpanded = value;
NotifyPropertyChanged("IsExpanded");
}
}
}
public Node(object value)
{
Value = value;
Children = new ObservableCollection<Node>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
After that, I subclassed the TreeView and TreeViewItem controls (I lose the custom theme on my treeview, but whatever...)
public class BindableTreeView : TreeView
{
protected override DependencyObject GetContainerForItemOverride()
{
var itm = new BindableTreeViewItem();
itm.SetBinding(TreeViewItem.IsExpandedProperty, new Binding("IsExpanded") { Mode = BindingMode.TwoWay });
return itm;
}
}
public class BindableTreeViewItem : TreeViewItem
{
protected override DependencyObject GetContainerForItemOverride()
{
var itm = new BindableTreeViewItem();
itm.SetBinding(TreeViewItem.IsExpandedProperty, new Binding("IsExpanded") { Mode = BindingMode.TwoWay });
return itm;
}
}
In my XAML, I just have to use BindableTreeView instead of TreeView, and it works.
The trick is to use SetterValueBindingHelper from here. Then your XAML will look like the following. Make sure you carefully copy what I have below.
<sdk:TreeView.ItemContainerStyle>
<Style TargetType="sdk:TreeViewItem">
<Setter Property="local:SetterValueBindingHelper.PropertyBinding">
<Setter.Value>
<local:SetterValueBindingHelper>
<local:SetterValueBindingHelper Property="IsSelected" Binding="{Binding Mode=TwoWay, Path=IsSelected}"/>
<local:SetterValueBindingHelper Property="IsExpanded" Binding="{Binding Mode=TwoWay, Path=IsExpanded}"/>
</local:SetterValueBindingHelper>
</Setter.Value>
</Setter>
</Style>
</sdk:TreeView.ItemContainerStyle>
The syntax isn't exactly like what you would use in WPF, but it works and it works well!

Categories

Resources