Chaining (Dependency) Properties to nested control - c#

I have a ListBox with a GroupStyle that contains another ListBox.
Now I want to filter the Items of the nested ListBox depending on the group name of the parent ListBox.
In the code below I tried to chain the GroupItem.Name.Name property to the GroupName property of the ViewModel of the nested ListBox, but this didn't work out so well.
Essentially the GroupNameIn Property is filled by the GroupItems' name(the TextBlock Text) and then sets the GroupNameOut Property to the same value in the PropertyChangedCallback. But the problem is that the GroupName Property of the NestedItemsViewModel to which GroupNameOut is bound to doesn't update.
Are there some mistakes in my approach or is there even a simpler/better way to achieve this behavior?
I would be very grateful if someone could point me in the right direction.
GroupStyle of the parent ListBox:
<Style x:Key="MyListBoxGroupStyle" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel Name="container" Width="Auto" Orientation="Vertical">
<TextBlock Name="groupNameTextBlock" Text="{Binding Path=Name.Name}"/>
<ItemsPresenter/>
<MyNestedListBox
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, Path=NestedItemsDataContext}"
ItemsSource="{Binding NestedItems}"
GroupNameIn="{Binding ElementName=groupNameTextBlock, Path=Text}"
GroupNameOut="{Binding Path=GroupName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The nested ListBox:
public class MyNestedListBox : ListBox
{
static MyNestedListBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyNestedListBox), new FrameworkPropertyMetadata(typeof(MyNestedListBox)));
}
public string GroupNameIn
{
get { return (string)GetValue(GroupNameInProperty); }
set { SetValue(GroupNameInProperty, value); }
}
public string GroupNameOut
{
get { return (string)GetValue(GroupNameOutProperty); }
set { SetValue(GroupNameOutProperty, value); }
}
// DepenencyProperties
public static readonly DependencyProperty GroupNameInProperty =
DependencyProperty.Register("GroupNameIn", typeof(string), typeof(MyNestedListBox), new UIPropertyMetadata(null) { PropertyChangedCallback = (obj, target) =>
{
obj.SetValue(GroupNameOutProperty, target.NewValue);
}
});
public static readonly DependencyProperty GroupNameOutProperty =
DependencyProperty.Register("GroupNameOut", typeof(string), typeof(MyNestedListBox), new UIPropertyMetadata(null));
}
ViewModel bound to the nested ListBox:
public class NestedItemsViewModel : ViewModelBase
{
private string _groupName;
public ObservableCollection<NestedItem> NestedItems { get; set; }
public string GroupName
{
get
{
return _groupName;
}
set
{
_groupName = value;
OnPropertyChanged(() => GroupName);
}
}
public NestedItemsViewModel()
{
NestedItems = new ObservableCollection<NestedItem>();
}
}

Related

Why is my Child View not displayed when I show my Container View in the MainWindow with WPF ReactiveUI?

I am currently learning my way around WPF and ReactiveUI. I try to explain my problem with a simplified example. I have a ContainerView that contains a ChildView and is bound to a ContainerViewModel. The ContainerViewModel has a property Child that is bound to the ChildView control. The ChildView control has a corresponding ChildViewModel.
I also have a style for my ContainerView that sets the Template property to a ControlTemplate with a Grid that contains a ViewModelViewHost control named "Child".
However, when I show the ContainerView in the MainWindow, the ChildView is not displayed.
Here's the code for the ContainerView/ViewModel and its style:
public class ContainerView : Control, IViewFor<ContainerViewModel>
{
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
nameof(ViewModel), typeof(ContainerViewModel), typeof(ContainerView), new PropertyMetadata(default(ContainerViewModel)));
public ContainerViewModel ViewModel
{
get { return (ContainerViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (ContainerViewModel)value;
}
public ContainerView()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Child, view => view.Child.ViewModel);
});
}
private ViewModelViewHost Child { get; set; }
public override void OnApplyTemplate()
{
Child = GetTemplateChild(nameof(Child)) as ViewModelViewHost;
}
}
public class ContainerViewModel : ReactiveObject
{
private ChildViewModel _child;

public ChildViewModel Child
{ 
get => _child;
set => this.RaiseAndSetIfChanged(ref _child, value);
}
 
public ContainerViewModel()
 {
Child = new ChildViewModel();
}
}
<Style TargetType="{x:Type views:ContainerView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:ContainerView">
<Grid>
<reactiveUi:ViewModelViewHost x:Name="Child"></reactiveUi:ViewModelViewHost>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And here's the code for the "ChildView" and its style:
public class ChildView : Control, IViewFor<ChildViewModel>
{
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
nameof(ViewModel), typeof(ChildViewModel), typeof(ChildView), new PropertyMetadata(default(ChildViewModel)));
public ChildViewModel ViewModel
{
get { return (ChildViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (ChildViewModel)value;
}
public ChildView()
{
DefaultStyleKey = nameof(ChildView);
}
}
<Style TargetType="{x:Type views:ChildView}">
<Setter Property="Background" Value="Brown"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:ChildView">
<Ellipse Width="200" Height="200" Fill="Yellow" ></Ellipse>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Any idea why the ChildView is not displayed?
Ok, I think i got it.. I don't no if this is the best solution, but it works for me. I had to instantiate the ViewModel in the View by myself.
public ContainerView()
{
this.ViewModel = new ContainerViewModel();
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Child, view => view.Child.ViewModel);
});
}

How to Bind ItemsSource of ComboBox in ControlTemplate?

Situation
I have to change the content of the Flyout Item in GridView. So I am creating ControlTemplate in Page.Resources and setting it in ContentControl which is inside Flyout.
Problem
I have a ComboBox in ControlTemplate. Now I want to set the ItemsSource of ComboBox to List<string> (_easingType) which is declared in MainPage
Question
How to Bind/Set ItemsSource of ComboBox in ControlTemplate?
Code
I have removed the unnecessary parts of the code
XAML
<Page.Resources>
<ControlTemplate x:Key="BlurEditFlyout">
....
<ComboBox ItemsSource="{Bind it to the _esaingType}" />
....
<ControlTemplate x:Key="BlurEditFlyout">
</Page.Resources>
<GridView ItemsSource="{x:Bind _items}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:MethodData">
<StackPanel>
....
<Button Visibility="{x:Bind EditButtonVisibility}">
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
</Style>
</Flyout.FlyoutPresenterStyle>
<ContentControl Template="{x:Bind FlyoutTemplate}"/>
</Flyout>
</Button.Flyout>
<SymbolIcon Symbol="Edit"/>
</Button>
....
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Code Behind
public sealed partial class MainPage : Page
{
ObservableCollection<MethodData> _items = new ObservableCollection<MethodData>();
List<string> _easingType = new List<string>(Enum.GetNames(typeof(EasingType)).ToArray());
Dictionary<MethodName, ControlTemplate> _buttonFlyoutDictionary = new Dictionary<MethodName, ControlTemplate>();
public MainPage()
{
this.InitializeComponent();
LoadFlyoutResources();
_items.Add(GetMethodData(MethodName.Blur));
}
private void LoadFlyoutResources()
{
_buttonFlyoutDictionary.Add(MethodName.Blur, (ControlTemplate)Resources["BlurEditFlyout"]);
.....
}
private MethodData GetMethodData(MethodName methodName)
{
_buttonFlyoutDictionary.TryGetValue(methodName, out ControlTemplate flyoutTemplate);
return new MethodData(methodName, flyoutTemplate);
}
}
public class MethodData
{
public string Name { get; set; }
public ControlTemplate FlyoutTemplate { get; set; }
public Visibility EditButtonVisibility { get; set; }
public MethodData(MethodName name, ControlTemplate flyoutTemplate)
{
Name = name.ToString();
FlyoutTemplate = flyoutTemplate;
EditButtonVisibility = (name == MethodName.Then) ? Visibility.Collapsed : Visibility.Visible;
}
}
public enum MethodName
{
Blur,
....
}
Full Code
AnimationSetSamplePage.zip
Your DataContext in the Flyout control is actually each item in "_items". You'll want to create a DataContext proxy to get to your Page's DataContext. You can follow either of these two links to create the proxy.
https://weblogs.asp.net/dwahlin/creating-a-silverlight-datacontext-proxy-to-simplify-data-binding-in-nested-controls
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
The gist of it is you'll want to create a proxy that you can reference as a static resource. Following the first link, you'll do something like this:
public class DataContextProxy : FrameworkElement
{
public DataContextProxy()
{
this.Loaded += new RoutedEventHandler(DataContextProxy_Loaded);
}
void DataContextProxy_Loaded(object sender, RoutedEventArgs e)
{
Binding binding = new Binding();
if (!String.IsNullOrEmpty(BindingPropertyName))
{
binding.Path = new PropertyPath(BindingPropertyName);
}
binding.Source = this.DataContext;
binding.Mode = BindingMode;
this.SetBinding(DataContextProxy.DataSourceProperty, binding);
}
public Object DataSource
{
get { return (Object)GetValue(DataSourceProperty); }
set { SetValue(DataSourceProperty, value); }
}
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register("DataSource", typeof(Object), typeof(DataContextProxy), null);
public string BindingPropertyName { get; set; }
public BindingMode BindingMode { get; set; }
}
You should use public access modifier for _easingType
public List<string> _easingType = new List<string>(Enum.GetNames(typeof(EasingType)).ToArray());
In MainPage.xaml
<Page.Resources>
<local:DataContextProxy x:Key="DataContextProxy" />
<ControlTemplate x:Key="BlurEditFlyout">
....
<ComboBox ItemsSource="{Binding Source={StaticResource DataContextProxy}, Path=DataSource._easingType}" />
....
<ControlTemplate x:Key="BlurEditFlyout">
</Page.Resources>
...
How to Bind/Set ItemsSource of ComboBox in ControlTemplate?
I'm not sure if you have deep reasons for asking this question, but directly to reply this question, we could just set the string list _esaingType as the value of DataContext property and binding it. For example:
XAML
<Page.Resources>
<ControlTemplate TargetType="FlyoutPresenter" x:Key="BlurEditFlyout" >
...
<ComboBox ItemsSource="{Binding}" />
...
</ControlTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button>
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="Template" Value="{StaticResource BlurEditFlyout}">
</Setter>
</Style>
</Flyout.FlyoutPresenterStyle>
<!--<ContentControl Template="{StaticResource BlurEditFlyout}"/>-->
</Flyout>
</Button.Flyout>
<SymbolIcon Symbol="Edit"/>
</Button>
</Grid>
Code behind
List<string> _easingType = new List<string>();
public MainPage()
{
this.InitializeComponent();
_easingType.Add("test2");
_easingType.Add("test1");
this.DataContext = _easingType;
}
Any concerns on this way or any issues when using this on your side please kindly tell me I will follow up in time. More details please reference Data binding in depth.

WPF Custom Control with child ItemsControl not showing items

I'm creating a complex custom control which uses both the Items property of the inherited control and also the SubItems of an additional embedded ItemsControl. Please find below a stripped code extract:
public class MyControl : ItemsControl
{
public static readonly DependencyProperty SubItemsProperty = DependencyProperty.Register("SubItems", typeof(ObservableCollection<object>), typeof(MyControl), new PropertyMetadata(null));
public ObservableCollection<object> SubItems
{
get { return (ObservableCollection<object>)GetValue(SubItemsProperty); }
set { SetValue(SubItemsProperty, value); }
}
public MyControl() : base()
{
SubItems = new ObservableCollection<object>();
}
static MyControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyControl), new FrameworkPropertyMetadata(typeof(MyControl)));
}
}
And with the following Template (from Generic.xaml):
<Style TargetType="{x:Type my:MyControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type my:MyControl}">
<Grid>
<ItemsControl ItemsSource="{Binding SubItems, RelativeSource={RelativeSource TemplatedParent}}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Items will be added dynamically to the SubItems Collection.
The control works fine in the control's constructor, the SubItems are set to a non-empty collection. For example by setting SubItems = new ObeservableCollection<object>() { "" }. What could be the reason for this?

Bind Command to ContextMenu items with HierarchicalDataTemplate

I have a Button which shows a ContextMenu look like
Here the XAML:
<Window.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Data.OnSelected, Source={StaticResource proxy}}" />
</Style>
<Button Name="ButtonMenu_Export"
Click="ButtonMenu_Export_Click"
Visibility="{Binding ButtonExportEnabled,
Converter={StaticResource VisibilityConverter}}">
<StackPanel Orientation="Vertical">
<Image Source="...." />
<TextBlock Width="70" TextAlignment="Center" Text="Export" />
</StackPanel>
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding ExportMenuItems}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemContainerStyle="{StaticResource MenuItemStyle}">
<ContentPresenter Content="{Binding Text}" RecognizesAccessKey="True" />
<HierarchicalDataTemplate.ItemsSource>
<Binding Path="SubItems" />
</HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
</Button.ContextMenu>
</Button>
The menu is created at runtime using this List (as in this article)
public System.Collections.Generic.List<MenuItem> ExportMenuItems
{
get { return _menuService.GetParentMenuItems(); }
}
Now, what I cannot do is bind the items to the OnSelected command of MenuItem class.
The class which defines the menu is:
public class MenuItem
{
private string name;
private string text;
private int menuId;
private ICommand onSelected;
private MenuItem parent;
private ObservableCollection<MenuItem> subItems;
public MenuItem(string name, string text, int MenuId)
{
this.menuId = MenuId;
this.name = name;
this.text = text;
this.subItems = new ObservableCollection<MenuItem>();
}
public string Name { get { return this.name; } }
public string Text { get { return this.text; } }
public MenuItem Parent { get { return this.parent; } set { this.parent = value; } }
public ICommand OnSelected
{
get
{
if (this.onSelected == null)
this.onSelected = new MenuCommand(this.ItemSelected, this.ItemCanBeSelected, menuId);
return this.onSelected;
}
}
public ObservableCollection<MenuItem> SubItems
{
get { return this.subItems; }
}
}
I created a proxy class as in this article to made DataContext visible to HierarchicalDataTemplate content but maybe I misunderstood something:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Where I'm wrong?
Change your command binding
Command="{Binding Data.OnSelectedd,Source={StaticResource proxy}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=MenuItem}"

Access item in observable collection on item selected

I have a WPF TreeView populated by an observable collection using a hiarchialdatabinding
I need to access the item in my observable collection or the database that was used to populate it.
An example use case is that the user right clicks a treeview item to add a subgroup. I obviously need to access its parent to add the child.
Any suggestions? Im so lost..
I cant just edit the treeview item itself cause then the changes wont reflect back to my database
Database Code:
[Serializable]
public class LoginGroup
{
public string Name { get; set; }
public Guid ID { get; set; }
public List<Login> LoginItems = new List<Login>();
public List<LoginGroup> Children { get; set; }
}
public static ObservableCollection<LoginGroup> _GroupCollection = new ObservableCollection<LoginGroup>();
public ObservableCollection<LoginGroup> GroupCollection
{
get { return _GroupCollection; }
}
TreeView:
<TreeView x:Name="groupView" Width="211" TreeViewItem.Selected="OnTreeItemSelected" DockPanel.Dock="Left" Height="Auto" ItemsSource="{Binding GroupCollection}" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
You can just cast the SelectedItem to LoginGroup:
LoginGroup selectedGroup = (LoginGroup)groupView.SelectedItem;
You can't reflect back changed of your properties because they don't have way to "notice" back that they are edited. You need inherit LoginGroup from DependencyObject or implement INotifyPropertyChanged
You should use TreeView's ItemContainer style.
Here's sample TreeNode view model:
public class TreeNode : ViewModel
{
public TreeNode()
{
this.children = new ObservableCollection<TreeNode>();
// the magic goes here
this.addChildCommand = new RelayCommand(obj => AddNewChild());
}
private void AddNewChild()
{
// create new child instance
var child = new TreeNode
{
Name = "This is a new child node.",
IsSelected = true // new child should be selected
};
// add it to collection
children.Add(child);
// expand this node, we want to look at the new child node
IsExpanded = true;
}
public String Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged("Name");
}
}
}
private String name;
public Boolean IsSelected
{
get { return isSelected; }
set
{
if (isSelected != value)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
private Boolean isSelected;
public Boolean IsExpanded
{
get { return isExpanded; }
set
{
if (isExpanded != value)
{
isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
}
private Boolean isExpanded;
public ObservableCollection<TreeNode> Children
{
get { return children; }
}
private ObservableCollection<TreeNode> children;
public ICommand AddChildCommand
{
get { return addChildCommand; }
}
private RelayCommand addChildCommand;
}
Some comments:
ViewModel is any base implementation of INotifyPropertyChanged
interface.
RelayCommand (a.k.a. DelegateCommand) is ICommand implementation for use in MVVM approach.
Here's the view:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<TreeView ItemsSource="{Binding}">
<TreeView.ItemContainerStyle>
<!-- Let's glue our view models with the view! -->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<!-- Here's menu item, which is responsible for adding new child node -->
<MenuItem Header="Add child..." Command="{Binding AddChildCommand}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Window>
... and sample data context initialization:
public MainWindow()
{
InitializeComponent();
DataContext = new ObservableCollection<TreeNode>
{
new TreeNode { Name = "Root", IsSelected = true }
};
}
Hope this helps.
Upd.
Of course, you have to expose child nodes as the ObservableCollection too. Otherwise, changes made to nodes collection won't be reflected.

Categories

Resources