WPF Custom Control with child ItemsControl not showing items - c#

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?

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.

TabControl inside CustomControl loads with no selected tab item

I wrote a CustomControl that supports direct content. When I nest a TabControl in it, there's no tab item selected on launch. It happens only when using ItemsSource. Anyone know what's wrong?
Sample implementation
In MainWindow.xaml:
<Window.DataContext>
<local:VM/>
</Window.DataContext>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TabControl ItemsSource="{Binding Data}"/>
<local:CustomControl1 Grid.Column="1">
<TabControl ItemsSource="{Binding Data}" />
</local:CustomControl1>
</Grid>
VM.cs
class VM
{
public List<int> Data { get; set; } = new List<int>{ 1, 2, 3, 4 };
}
CustomControl1:
[ContentProperty("Content")]
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public FrameworkElement Content
{
get { return (FrameworkElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(FrameworkElement), typeof(CustomControl1), new PropertyMetadata(null));
}
In Generic.xaml:
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
After some time of poking around I've found a solution, but I am not sure of the exact reasons behind this behavior.
I've modified your example and compared the behavior when the TabControl was put inside a ContentControl and a CustomControl1. Predictably, the first tab in case of ContentControl was selected, but in case of CustomControl1 it was not. After inspecting the ContentControl source code (in particular the ContentControl.OnContentChanged method) we can see that what it does is it sets its content as its logical child.
I then confirmed that setting the TabControl as the CustomControl1's logical child does the trick (but I'm not sure why, as I mentioned). So the minimal solution to your problem is to handle the logical relation between your control and its content in the property changed callback, e.g.:
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register(
"Content",
typeof(FrameworkElement),
typeof(CustomControl1),
new PropertyMetadata(null)
{
PropertyChangedCallback = OnContentChanged
});
private static void OnContentChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = (CustomControl1)d;
if (e.OldValue != null)
control.RemoveLogicalChild(e.OldValue);
if (e.NewValue != null)
control.AddLogicalChild(e.NewValue);
}
Note though that this lacks some checks (e.g. if new value already does have a logical parent or the control is a part of a template), so you might want to simply copy the code from the referenced source, or fallback to deriving your control from ContentControl altogether.

Custom WPF DataGrid with optional button column

I am creating a custom DataGrid control which has a property, ShowCloneColumn. If you set this property to true the DataGrid should add another column with a Button.
The class I've created derives from DataGrid and implemented a Dependency Property, ShowCloneColumn.
public static readonly DependencyProperty ShowCloneColumnProperty =
DependencyProperty.Register("ShowCloneColumn",
typeof(bool),
typeof(CloneRowDataGrid),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnShowCloneColumnPropertyChanged));
public bool ShowCloneColumn
{
get { return (bool) GetValue(ShowCloneColumnProperty); }
set { SetValue(ShowCloneColumnProperty, value); }
}
In Generic.xaml I have the following Style.
<!-- Somewhere in here a button column should be declared? -->
<Style TargetType="{x:Type uiControls:CloneRowDataGrid}" BasedOn="{StaticResource {x:Type DataGrid}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type uiControls:CloneRowDataGrid}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ShowCloneColumn" Value="True">
<!-- Show clone column, a column with a button -->
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I'm not so strong yet with templates and custom controls so I'm not so sure where to add the button column so that it will be visible when someone uses the CloneRowDataGrid and sets the dependency property ShowCloneColumn to true.
I had to remove the section from the Generic.xaml style for the DataGrid to properly layout and created the column in code.
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
CloneColumn.Visibility = ShowCloneColumn ? Visibility.Visible : Visibility.Hidden;
}
private DataGridTemplateColumn _cloneColumn;
private DataGridTemplateColumn CloneColumn
{
get
{
if (_cloneColumn == null)
{
_cloneColumn = new DataGridTemplateColumn
{
Header = string.Empty,
Visibility = ShowCloneColumn ? Visibility.Visible : Visibility.Hidden
};
FrameworkElementFactory buttonFactory = new FrameworkElementFactory(typeof (Button));
buttonFactory.SetValue(Button.ContentProperty, "Clone");
buttonFactory.AddHandler(Button.ClickEvent, new RoutedEventHandler(CloneButtonClicked));
DataTemplate textTemplate = new DataTemplate {VisualTree = buttonFactory};
_cloneColumn.CellTemplate = textTemplate;
Columns.Add(_cloneColumn);
}
return _cloneColumn;
}
}

Chaining (Dependency) Properties to nested control

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>();
}
}

Categories

Resources