Bind Canvas.Left and Canvas.Top in Style - wpf

I am trying to reflect a position from my view-model expressing the location of an object in my view by binding the Canvas.Left and Canvas.Top properties in the style of the item view to appropriate properties in the view-model. However, the bindings do not seem to work.
For this minimal sample, I have simplified the structure so there only is one control Thing that is styled and templated:
using System;
using System.Windows;
using System.Windows.Controls;
namespace LocationBinding
{
public class Thing : Control
{
static Thing()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Thing), new FrameworkPropertyMetadata(typeof(Thing)));
}
public Point Location {
get {
return new Point(70, 70);
}
}
public double VPos {
get {
return 100;
}
}
}
}
For the sake of simplicity, I have declared the style in the resource dictionary of the main window - a simple window with a canvas on it (my real project has this in Themes\Generic.xaml). In its style, I am binding to property values of the control:
<Window x:Class="LocationBinding.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LocationBinding"
Title="LocationBinding" Height="300" Width="300">
<Window.Resources>
<Style TargetType="local:Thing">
<Setter Property="Panel.ZIndex" Value="542"/>
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding VPos}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Thing">
<Ellipse Fill="ForestGreen" Width="30" Height="30"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Canvas Name="cnv">
</Canvas>
</Window>
The code-behind of the main window simply adds a Thing instance to the canvas:
using System;
using System.Windows;
namespace LocationBinding
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
cnv.Children.Add(new Thing());
}
}
}
The style is correctly applied, as evidenced by the ZIndex value (according to Snoop) and the correct template-based appearance of the control. However, Canvas.Left and Canvas.Top remain unset (and thus the Thing sticks in the top left corner of the canvas), even though according to threads such as this or this, Property="Canvas.Left" seems to be the correct syntax to refer to the attached property in a style.
I was first trying to bind Canvas.Top to Location.Y and replaced that with the VPos property in case the problem is related to binding to struct properties, but that does not seem to change anything, either.
What am I missing; how can I bind Canvas.Left and Canvas.Top in my style to the coordinates from my Thing.Location property?

By default, binding will search for property in DataContext of control but Location is your control (Thing) property.
So you need to use RelativeSource with Mode set to Self to tell binding engine to search for property in control itself and not in DataContext of control:
<Setter Property="Canvas.Left" Value="{Binding Location.X,
RelativeSource={RelativeSource Self}}"/>

Related

WPF binding to ViewModel property from the DataTemplate Style

I am trying to bind ForeGround color of all TextBlock items to a ViewModel property. The TextBlock elements locate under a Grid that itself is defined under DataTemplate. This whole code is defined under a UserControl.
I am trying to use RelativeSource binding to find the UserControl's DataContext and get the property I need.
XAML:
<my:MapControl>
<my:MapControl.Resources>
<ResourceDictionary>
<DataTemplate x:Key="SomeTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="TextElement.Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=DataContext.TextColor}" />
</Style>
</Grid.Style>
<TextBlock Grid.Column="0" />
<TextBlock Grid.Column="1" />
</Grid>
</DataTemplate>
</ResourceDictionary>
</my:MapControl.Resources>
</my:MapControl>
ViewModel:
public class MapViewModel
{
public virtual string TextColor
{
get { return _textColor; }
set
{
_textColor = value;
this.RaisePropertyChanged("TextColor");
}
}
private string _textColor = "Black";
}
The above binding doesn't work. If I change the Value binding to a hard-coded value, like "Red" for example, the Foreground color on those TextBlocks are showing correctly.
How to get the binding to work with this setup?
Analysis
It seems the root cause — binding to an instance of the string type instead of an instance of the Brush type.
Some of the possible solutions:
Change the type of the TextColor property of the MapViewModel class to from the string type to the SolidColorBrush type and update the implementation of the MapViewModel class appropriately.
Create custom implementation of the IValueConverter interface which takes the string as the input and outputs an instance of the SolidColorBrush type.
What version of .NET are you using? Works fine with 4.5 but IIRC it didn't with earlier versions and you had to declare a solidcolorbrush explicitly:
<Style TargetType="Grid">
<Setter Property="TextElement.Foreground">
<Setter.Value>
<SolidColorBrush Color="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=DataContext.TextColor}" />
</Setter.Value>
</Setter>
</Style>
Whatever you do don't create a brush or any other UI resources in your viewmodel, it's a violation of MVVM.

How to get the ActualWidth and ActualHeight dynamically of a customcontrol?

I have a custom control, ccTextBlock placed inside a ScrollViewer. The customcontrol will be changing sizes (vertically) when different strings are sent to it through the binding. The custom control will remain on the display, but will change as text elsewhere on the screen is selected.
How can I obtain the actual width and height of the custom control only after and with each text string sent to it? (Using OnApplyTemplate() did not work as it seems to be called only once on the first construction of the custom control.)
Thanks for any replies.
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<wc:ccTextBlock Text="{Binding Text, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
</Grid>
</ScrollViewer>
Update: Perhaps a better way to phrase this question would be "How to get the height of an element when it is inside a ScrollViewer". Here is the definition of ccTextBlock:
public class ccTextBlock : Control
{
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
// Using a DependencyProperty as the backing store for Text. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(ccTextBlock), new UIPropertyMetadata(null));
/// <summary>
/// Constructor
/// </summary>
static ccTextBlock()
{
// Initialize as lookless control
DefaultStyleKeyProperty.OverrideMetadata(typeof(ccTextBlock), new FrameworkPropertyMetadata(typeof(ccTextBlock)));
}
public override void OnApplyTemplate()
{
//Effectively apply the template
base.OnApplyTemplate();
Console.WriteLine(String.Format(" ActualHeight is {0}", this.ActualHeight.ToString()));
var x = this.FontSize;
}
}
Where Generic.xaml is:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomControlLibrary">
<Style TargetType="{x:Type local:ccTextBlock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ccTextBlock}">
<!-- Control Layout -->
<TextBlock Text="{TemplateBinding Text}" TextWrapping="Wrap" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
ActualWidth and ActualHeight are the properties that contain the current width and height of the control.
If you are looking for an Event that notifies about changes, it would be the FrameworkElement.SizeChanged event. You could register for this event in the OnApplyTemplate implementation.

Expander.IsExpanded Binding breaking after first click

am probably doing something but can't figure out my problem. Any help would be really appreciated:
I am having a CustomControl called Section. A Section is collapsible and therefore its ControlTemplate contains an Expander. The Expanders IsExpanded-Property is bound to the Section's IsExpanded Property via TemplateBinding.
When setting IsExpanded on a Section the Expander collapses, but using the toggleButton within the Expander appears to break that binding. Probably by setting a local value to the Expander's IsExpanded-Property. Anyways, after changing the Expander state via Mouse the binding breaks and setting the Section's IsExpanded doesn't do anything.
This however does not happen when putting an Expander into a view and binding its IsExpanded-Property to some DP in the View.
Also notworthy: Snoop does not show any Bindings on the Expander's IsExpanded-Property. It only shows the Value-Source is ParentTemplate. As soon as I click the ToggleButton to Change IsExpanded the Value-Source changes to Local (possibly breaking the former Binding?)
Section.cs:
public class Section : Control
{
static Section()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Section), new FrameworkPropertyMetadata(typeof(Section)));
}
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
"IsExpanded", typeof (bool), typeof (Section), new PropertyMetadata(default(bool)));
public bool IsExpanded
{
get { return (bool) GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
}
Generic.xaml Style:
<Style TargetType="{x:Type local:Section}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Section}">
<Expander Header="Test" IsExpanded="{TemplateBinding IsExpanded}" >
<Rectangle Fill="Aqua" Height="200" Width="200" />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Any ideas?
So I found the answer which is pretty basic knowledge actually:
TemplateBindings are always ONEWAY no matter what the MetaData states...
Using:
IsExpanded="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded}"
fixed my problem...

Attached behavior binding to element in controltemplate

I am adding an attached behaviour to a slider which will cause it to scroll some content when the thumb is dragged and held over a specific region. (Can't use a straightforward IsMouseOver trigger as the Slider Thumb has MouseCapture.)
The behaviour has 3 properties:
#region IsScrollHoverProperty
public static readonly DependencyProperty IsScrollHoverProperty = DependencyProperty.RegisterAttached(
"IsScrollHover",
typeof(Boolean),
typeof(ScrollHoverAreaBehaviour),
new UIPropertyMetadata(false));
#endregion
#region ScrollLeftRectProperty
public static readonly DependencyProperty ScrollLeftRectProperty = DependencyProperty.RegisterAttached(
"ScrollLeftRect",
typeof(Rectangle),
typeof(ScrollHoverAreaBehaviour),
new UIPropertyMetadata(null));
#endregion
#region ScrollRightRectProperty
public static readonly DependencyProperty ScrollRightRectProperty = DependencyProperty.RegisterAttached(
"ScrollRightRect",
typeof(Rectangle),
typeof(ScrollHoverAreaBehaviour),
new UIPropertyMetadata(null));
#endregion
The IsScrollHoverProperty is being set to true when the user drags the slider, this is all done in the Slider's ControlTemplates.Triggers, and works correctly.
When it's set to true the callback is going to hook PreviewMouseEnterHandlers into the two Rectangles to detect when the mouse enters them.
The Rectangles in question are also defined in the Slider's controltemplate thusly:
<StackPanel Grid.Row="0" Grid.RowSpan="3" HorizontalAlignment="Left" Orientation="Horizontal">
<Rectangle Width="40" Fill="#AAAAAAAA" Name="ScrollLeftRect"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.RowSpan="3" HorizontalAlignment="Right" Orientation="Horizontal">
<Rectangle Width="40" Fill="#AAAAAAAA" Name="ScrollRightRect"/>
</StackPanel>
The problem I have is binding these Rectangles to the attached ScrollRightRect and ScrollLeftRect Properties. I have tried a few things and suspect I have made a stupid binding error or am trying to do something not allowed. I am currently binding them in the controltemplate.triggers as follows:
<Trigger Property="local:ScrollHoverAreaBehaviour.IsScrollHover" Value="False">
<Setter Property="local:ScrollHoverAreaBehaviour.ScrollLeftRect" Value="{Binding ElementName=ScrollLeftRect}"/>
<Setter Property="local:ScrollHoverAreaBehaviour.ScrollRightRect" Value="{Binding ElementName=ScrollRightRect}"/>
<Setter TargetName="ScrollLeftRect" Property="Fill" Value="Red"/>
<Setter TargetName="ScrollRightRect" Property="Fill" Value="Red"/>
</Trigger>
I know this Trigger is being tripped as the rectangles fill Red as expected.
Can anyone spot what I'm doing wrong from these snippets?
Thanks in advance.
Rob
First, let's confirm you're not doing anything wrong, and the problem has nothing to do with the attached behaviors.
<Button>
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="Yellow">
<StackPanel>
<TextBlock x:Name="theText" Text="Hello" />
<ContentPresenter />
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Content" Value="{Binding ElementName=theText, Path=Text}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
This snippet should cause "Hello" to appear twice when I mouse over the button, but it doesn't, and I get the same error as you:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=theText'. BindingExpression:Path=Text; DataItem=null; target element is 'Button' (Name=''); target property is 'Content' (type 'Object')
This is explainable - once the binding is set on the Button, it won't be able to find a control named 'theText', because the Button lives in a different NameScope.
An alternative
Some WPF controls need to do something similar to you - they assume that a specific control exists in the tree that they will interact with. But they don't use properties - they use names.
Start by giving the controls a name - the convention is to use "PART_" prefix:
<Rectangle ... Name="PART_ScrollLeftRect" />
Now put code like this in your callback when IsScrollHover is set:
private static void IsScrollHoverSetCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = (Slider) d;
if ((bool)e.NewValue == false)
return;
target.ApplyTemplate();
var leftRectangle = target.Template.FindName("PART_ScrollLeftRect", target);
var rightRectangle = target.Template.FindName("PART_ScrollRightRect", target);
// Do things with the rectangles
}
Note that depending on when the IsScrollHost property is set, the template might not be ready yet. In that case, you might want to subscribe to the Loaded or similar event, and then call ApplyTemplate().
Although it might seem more complicated, it has one nice benefit: the markup will be simpler. A designer using Blend won't have to remember to wire up those complicated triggers, they just have to name the controls correctly.
The use of the PART_ prefix is a WPF convention, and normally used along with the TemplatePart attribute. An example of this is the TextBox. When you override the template of a TextBox, it won't function until you add a control named PART_ContentHost.
Update: I just blogged about template parts here: http://www.paulstovell.com/wpf-part-names

Change databinding on WPF ComboBox when Checkbox value changes

I have a WPF ComboBox which is databound to a Collection, but depending on wether a Checkbox is checked or not I'd like to vary which Collection the ComboBox is bound to.
The basic issue is I have a large collection of MyCustomer and I also have a filtered collection of MyCustomer - the filtering is quite intensive and I don't want to do it with a CollectionView for the main reason that it is already done, the filtered collection already exists - hence the need to simply switch the databinding of the combo.
I'm hoping for a pure XAML solution, obviously writing some code behind would be a relatively simple solutions but it doesn't feel like it should be required.
Here's an example using a DataTrigger to switch the collections:
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel.Resources>
<x:Array x:Key="notes" Type="{x:Type sys:String}">
<sys:String>do</sys:String>
<sys:String>re</sys:String>
<sys:String>mi</sys:String>
</x:Array>
<x:Array x:Key="letters" Type="{x:Type sys:Char}">
<sys:Char>a</sys:Char>
<sys:Char>b</sys:Char>
<sys:Char>c</sys:Char>
</x:Array>
</StackPanel.Resources>
<CheckBox x:Name="chkLetters" Content="Use Letters"/>
<ListBox>
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Setter Property="ItemsSource" Value="{StaticResource notes}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=chkLetters}" Value="True">
<Setter Property="ItemsSource" Value="{StaticResource letters}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
</StackPanel>
For you these wouldn't be different arrays, but probably different CollectionViewSources with filters or something, but the principle is the same.
Best way I know is to use some shell collection that internally 'gets' the right collection.
So you have your UnfilteredCollection and your FilteredCollection and then a property called BindingCollection which, in its 'getter,' evaluates some state (the checkbox would be bound to this state) to determine which collection to retrieve.
If you use MVVM for the databinding between the UI and the collections, one way to do it would be like this:
<!-- Your ComboBox binds to some shell collection -->
<ComboBox ItemsSource="{Binding BindingCollection}" />
<!-- The input to this item will determine which collection is internally exposed -->
<CheckBox IsChecked="{Binding UseFilteredSet}" />
And then have your ViewModel (middle-layer) file do something like this (I'm not including the implementation details of INotifyPropertyChanged, but I can if you wish):
private ObservableCollection<MyCustomer> UnfilteredCollection
{
get { return _unfilteredCollection; }
}
private ObservableCollection<MyCustomer> FilteredCollection
{
get { return _filteredCollection; }
}
// The public collection to which your ComboBox is bound
public ObservableCollection<MyCustomer> BindingCollection
{
get
{
return UseFilteredSet ?
FilteredCollection :
UnfilteredCollection;
}
}
// CheckBox is bound to this state value, which tells the bindings on the shell
// collection to refresh when the value of this state changes.
public bool UseFilteredSet
{
get { return _useFilteredSet; }
set
{
_useFilteredSet = value;
OnPropertyChanged("UseFilteredSet");
OnPropertyChanged("BindingCollection");
}
}

Resources