I have an ItemsControl that is binding to an ObservableCollection "MenuButtons".
In the ItemsControl, I want to add some Buttons programmatically with Dependency Properties.
My problem is that the values I pass are not updated. The default values are displayed in the view.
C#
private void btnTest_Click(object sender, RoutedEventArgs e)
{
var vm = DataContext as UflMainWindowViewModel;
vm.MenuButtons.Add(new UflMenuButton { IconText="Test123", Style = (Style)Application.Current.Resources["UflMenuButtonStyle"] });
}
C# UflButtonClass
public class UflMenuButton : Button
{
public string IconText
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("IconText", typeof(string), typeof(UflMenuButton), new UIPropertyMetadata("default", new PropertyChangedCallback(IconTextChanged)));
private static void IconTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
UflMenuButton button = (UflMenuButton)sender;
button.IconText = (string)e.NewValue;
}
}
with the following Style:
WPF
<Style x:Key="UflMenuButtonStyle" TargetType="{x:Type local:UflMenuButton}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=(local:UflMenuButton.IconText),RelativeSource={RelativeSource TemplatedParent}}" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
You can bind to the first ancestor of type UflMenuButton :
<TextBlock Text="{Binding IconText, RelativeSource={RelativeSource AncestorType={x:Type local:UflMenuButton}}}" HorizontalAlignment="Center"/>
This work, but I assume a more elegant way exists.
Related
I have a custom ContentControl as follows.
Modal.xaml:
<Style TargetType="local:Modal">
<Setter Property="Background" Value="#65000000" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Modal">
<Grid Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
Visibility="{Binding IsOpen, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}">
<Border x:Name="ContentBorder"
Padding="80">
<ContentControl HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<Grid x:Name="ContentGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
CornerRadius="10">
<ContentPresenter Content="{TemplateBinding Content}"/>
</Grid>
</ContentControl>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
How can I access the ContentBorder or ContentGrid controls inside of the code behind. I am saying if (control.GetTemplateChild("ContentBorder") is Border border) and it can't find it. Please help.
Code Behind is as follows:
Modal.cs:
public class Modal : ContentControl
{
public Modal()
{
DefaultStyleKey = typeof(Modal);
}
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(Modal), new PropertyMetadata(false));
public bool IsOpen
{
get => (bool)GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
public static readonly DependencyProperty DialogMaxWidthProperty =
DependencyProperty.Register(nameof(DialogMaxWidth), typeof(double), typeof(Modal), new PropertyMetadata(0, OnMaxWidthSet));
private static void OnMaxWidthSet(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (Modal)d;
//GetTemplateChild is unable to find ContentBorder.
if (control.GetTemplateChild("ContentBorder") is Border border)
{
border.MaxWidth = (double)e.NewValue;
}
}
public double DialogMaxWidth
{
get => (double)GetValue(DialogMaxWidthProperty);
set => SetValue(DialogMaxWidthProperty, value);
}
}
I am using the custom control from somwhere outside as follows.
<controls:Modal x:Name="ContentModal"
Canvas.ZIndex="100"
Grid.Row="0"
Grid.RowSpan="2"
IsOpen="{x:Bind ViewModel.IsModalOpen, Mode=OneWay}"
DialogMaxWidth="450"/>
if DialogMaxWidth property change execute before OnApplyTemplate, GetTemplateChild will return null. so you must add a field and set it on OnApplyTemplate function. then check it on propertychaged
private Border brd;
public override void OnApplyTemplate()
{
brd = this.GetTemplateChild("ContentBorder") as Border;
if (brd!=null && DialogMaxWidth!=0)
brd.MaxWidth = DialogMaxWidth;
}
private static void OnMaxWidthSet(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (Modal)d;
//GetTemplateChild is unable to find ContentBorder.
if (control.brd != null && control.brd is Border)
{
control.brd.MaxWidth = (double)e.NewValue;
}
}
So when I use the below style, it is applied to my control as expected. However, the templates inside of the GridView (ItemsPanelTemplate and ItemsTemplate) look at the view model the consumer applies for its data context.
The problem is that I want to set the item dimensions in my control.
So my question is, how do I apply the control template as the data context to the ItemsPanelTemplate and the ItemTemplate?
My first thought was to use ancestral binding but that doesn't appear to be a feature in UWP.
My Control Class
public class FilterableImageWrapGrid : FilterableContentList
{
private GridView _partGridView;
public Point ItemDimensions
{
get { return (Point)GetValue(ItemDimensionsProperty); }
set { SetValue(ItemDimensionsProperty, value); }
}
// Using a DependencyProperty as the backing store for ItemDimensions. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemDimensionsProperty =
DependencyProperty.Register("ItemDimensions", typeof(Point), typeof(FilterableImageWrapGrid), new PropertyMetadata(new Point()));
public FilterableImageWrapGrid()
{
DefaultStyleKey = typeof(FilterableImageWrapGrid);
}
protected override void OnApplyTemplate()
{
_partGridView = GetTemplateChild("PART_FilterableImageList") as GridView;
base.OnApplyTemplate();
}
private static void OnItemDimensionsChanged(object sender, DependencyPropertyChangedEventArgs args)
{
FilterableImageWrapGrid wrapGrid = sender as FilterableImageWrapGrid;
if (wrapGrid != null && wrapGrid._partGridView != null)
{
wrapGrid._partGridView.ItemTemplate.SetValue(GridViewItem.WidthProperty, wrapGrid.ItemDimensions.X);
wrapGrid._partGridView.ItemTemplate.SetValue(GridViewItem.HeightProperty, wrapGrid.ItemDimensions.Y);
}
}
}
My style in my Generic.xaml file
<Style TargetType="controls:FilterableImageWrapGrid">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid VerticalAlignment="Stretch">
<GridView
x:Name="PART_FilterableImageList"
ItemsSource="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=FilteredItems, Mode=TwoWay}"
SelectedItem="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=SelectedContentItem, Mode=TwoWay}">
<GridView.ItemContainerTransitions>
<TransitionCollection>
<EntranceThemeTransition IsStaggeringEnabled="True"/>
<AddDeleteThemeTransition />
<EdgeUIThemeTransition Edge="Left"/>
</TransitionCollection>
</GridView.ItemContainerTransitions>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid
x:Name="PART_ItemsWrapGrid"
ItemHeight="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=ItemDimensions.Y}"
ItemWidth="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=ItemDimensions.X}"
Margin="2" Orientation="Horizontal"
HorizontalAlignment="Center"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate>
.... Data template that binds to the view model the consumer provides....
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
If I understand you correctly it doesn't work to have the RelativeSource Mode=TemplatedParent binding in a template inside a template. Is that it?
then define the ItemDimensions property as an attached Dependency Property:
public class FilterableImageWrapGrid : FilterableContentList
public static Point GetItemDimensions(DependencyObject obj)
{
return (Point)obj.GetValue(ItemDimensionsProperty);
}
public static void SetItemDimensions(DependencyObject obj, Point value)
{
obj.SetValue(ItemDimensionsProperty, value);
}
public static readonly DependencyProperty ItemDimensionsProperty =
DependencyProperty.RegisterAttached("ItemDimensions", typeof(Point), typeof(ItemsWrapGrid), new PropertyMetadata(new Point()));
...
}
and then add this to the template:
<GridView
x:Name="PART_FilterableImageList"
...
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid
x:Name="PART_ItemsWrapGrid"
ItemHeight="{Binding RelativeSource=
{RelativeSource Mode=Self}, Path=ItemDimensions.Y}"
ItemWidth="{Binding RelativeSource=
{RelativeSource Mode=Self}, Path=ItemDimensions.X}"
Margin="2" Orientation="Horizontal"
HorizontalAlignment="Center"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
...
</GridView>
That would be like an inheritable property (which don't exist in uwp (yet?)) that you manually push downward the visual tree to the inner template with the template binding.
I have the following custom control based on the "heavy option" at this link:
public partial class SelectableContentControl : ContentControl
{
public SelectableContentControl()
{
InitializeComponent();
var isCheckedDesc = DependencyPropertyDescriptor.FromProperty(IsCheckedProperty, typeof(SelectableContentControl));
isCheckedDesc.AddValueChanged(this, IsCheckedPropertyChanged);
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register("IsChecked", typeof(bool),
typeof(SelectableContentControl), new PropertyMetadata(false));
private void IsCheckedPropertyChanged(object sender, EventArgs e)
{
var selectable = Content as IAmSelectable;
if (selectable != null) selectable.IsSelected = IsChecked;
}
}
The style defined for the SelectableContentControl is as follows:
<Style TargetType="{x:Type controls1:SelectableContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls1:SelectableContentControl}">
<CheckBox IsChecked="{TemplateBinding IsChecked}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
...and my usage:
<controls:SelectableContentControl Grid.Row="2" Content="{Binding Dummy}" IsChecked="{Binding Dummy.IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
I want IsCheckedPropertyChanged to be called whenever the IsChecked value changes on the UI, but this isn't happening. Anyone see what I'm missing?
TemplateBinding works in a OneWay mode, meaning that the value is updated only in source-to-target direction (your control being the source, and the CheckBox inside the template the target). If you want the binding to work in TwoWay mode, you should use a normal Binding instead:
<ControlTemplate TargetType="{x:Type controls1:SelectableContentControl}">
<CheckBox IsChecked="{Binding IsChecked, RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
Note that you don't need to specify Mode=TwoWay on the binding, because CheckBox.IsChecked property binds in two-way mode by default.
See this question for more detailed info.
I am attempting to reach this situation:
Have a ListBox with each cell containing a CheckBox and a TextBox, via DataTemplate
Make the list selectable, ie. I can bind the SelectedItems to a collection in my VM.
Link those selections to the status of the Checkbox(checked, unchecked).
Whenever the user types something in the TextBox, the Checkbox will be checked and selected, and vice versa, when the string is empty, it will be deselected.
I managed to get all of these criterias seperately like this:
Bind to SelectedItems using this solution.
Bind the CheckBox.IsChecked to:
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}" Path="IsSelected" Mode="OneWayToSource"/>
Bind the CheckBox.IsChecked to:
<Binding Path="Text" ElementName="MyTextBox" Converter="{View:EmptyStringToBooleanConverter}" Mode="OneWay"/>
The thing is I can't get both of these bindings to work together. I have tried other solutions like DataTriggers, but they were not helpful because IsSelected is not accessible and because I need to bind to something inside the DataTemplate.
I am really trying to avoid having to add a property "IsSelected" to my class (represented by the DataTemplate).
Please help, I am willing to hear crazy suggestions as long as they're MVVM-y.
Thanks!
In general:
checkbox is unchecked by default
when you type string "checked", the checkbox will be checked, otherwise will stay unchecked
hope it helps.
XAML
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:clr="clr-namespace:WpfApplication1"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<clr:StringToBooleanConverter x:Key="StringToBooleanConverter"/>
</Grid.Resources>
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chb" IsChecked="{Binding Text, ElementName=txt, Mode=OneWay, Converter={StaticResource StringToBooleanConverter}}"/>
<TextBox x:Name="txt" Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Width="150"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Code-Behind
Imports System.Collections.ObjectModel
Class MainWindow
Public Sub New()
InitializeComponent()
Me.DataContext = New ObservableCollection(Of DummyClass)(Enumerable.Range(0, 10).Select(Function(a) New DummyClass() With {.Title = "Dummy Title: " & a}))
End Sub
End Class
Public Class DummyClass
Property Title As String
End Class
Public Class StringToBooleanConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert
Dim strValue = System.Convert.ToString(value)
If String.IsNullOrEmpty(strValue) Then
Return False 'Unchecked
End If
If strValue = "checked" Then
Return True 'checked
End If
Return False 'default
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class
This is the XAML code:
<ListBox ItemsSource="{Binding MyList}" SelectionMode="Multiple" Width="200">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel Margin="2">
<CheckBox DockPanel.Dock="Left" IsChecked="{Binding IsSelected}"/>
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" Background="Transparent"/>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
And its code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
for (int i = 0; i < 100; i++)
{
MyList.Add(new ViewModel());
}
}
//MyList Observable Collection
public ObservableCollection<ViewModel> MyList { get { return _myList; } }
private ObservableCollection<ViewModel> _myList = new ObservableCollection<ViewModel>();
}
ViewModel class (each item):
public class ViewModel : DependencyObject
{
//Text Dependency Property
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(ViewModel),
new UIPropertyMetadata(null, (d, e) =>
{
((ViewModel)d).IsSelected = !string.IsNullOrWhiteSpace((string)e.NewValue);
}));
//IsSelected Dependency Property
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(ViewModel),
new UIPropertyMetadata(false, (d, e) =>
{
}));
}
Given the control below, how do I modify it to accept "Run" text?
Custom Control:
[ContentProperty("Text")]
public class GradientTitle : Control
{
public GradientTitle()
{
this.DefaultStyleKey = typeof(GradientTitle);
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(GradientTitle), new PropertyMetadata(null));
}
Intended use:
<customControls:GradientTitle>
<Run Text="The quick brown fox" />
<Run Text="jumps over the lazy dog" />
<Run Text="{Binding SomeText}" />
</customControls:GradientTitle>
You probably shouldn't do this, as TextBlock already does it, but anyway:
[ContentProperty("Inlines")]
[TemplatePart(Name = "PART_InlinesPresenter", Type = typeof(TextBlock))]
public class GradientTitle : Control
{
private readonly Collection<Inline> _inlines = new Collection<Inline>();
public Collection<Inline> Inlines
{
get { return _inlines; }
}
static GradientTitle()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(GradientTitle),
new FrameworkPropertyMetadata(typeof(GradientTitle)));
}
public override void OnApplyTemplate()
{
base.ApplyTemplate();
var inlinesPresenter = GetTemplateChild("PART_InlinesPresenter") as TextBlock;
if(inlinesPresenter != null)
{
var targetInlines = inlinesPresenter.Inlines;
foreach(var inline in Inlines)
{
targetInlines.Add(inline);
}
}
}
}
To simplify solution, I'm using TextBlock to render inline objects and declaring Inlines as a simple (non-dependency) property (almost as TextBlock does - it's Inlines property is not bindable without some external help). Also I don't track any collection changes. All these missing features can be added if needed, but require too much code for a simple answer.
Usage in XAML:
<Grid>
<FrameworkElement.Resources>
<ResourceDictionary>
<Style TargetType="{x:Type local:GradientTitle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:GradientTitle}">
<TextBlock x:Name="PART_InlinesPresenter" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</FrameworkElement.Resources>
<customControls:GradientTitle>
<Run Text="TEST1" />
<LineBreak />
<Run Text="TEST2" />
<LineBreak />
<Run Text="{Binding Path=Title, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</customControls:GradientTitle>
</Grid>
Obviously, style can be declared somewhere else.