WPF DataContext works differently in seemingly identical situations - c#

I have the following resources:
<Window.Resources>
<SolidColorBrush x:Key="b" Color="{Binding B}" />
<my:C x:Key="c" Prop="{Binding Source={StaticResource b}}" />
<my:C x:Key="d" Prop="{Binding A}" />
<Ellipse x:Key="e" Fill="{Binding A}" />
<Ellipse x:Key="f">
<Ellipse.Fill>
<SolidColorBrush Color="{Binding B}" />
</Ellipse.Fill>
</Ellipse>
</Window.Resources>
My window has a data context declared like this:
<Window ... DataContext="{my:Context}" ...>
Custom classes C and Context are defined like this:
public class Context : MarkupExtension
{
public Brush A { get; } = Brushes.Blue;
public Color B { get; } = Colors.Red;
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}
public class C : DependencyObject
{
public static readonly DependencyProperty PropProperty = DependencyProperty.Register("Prop", typeof(Brush), typeof(C));
public Brush Prop { get { return (Brush)GetValue(PropProperty); } set { SetValue(PropProperty, value); } }
}
Now, the ways in which I use my data context and binding seem very similar to me, yet if I check my resources with the following code (inside a button click handler)
MessageBox.Show("f: " + ((FindResource("f") as Ellipse).Fill?.ToString() ?? "null"));
MessageBox.Show("e: " + ((FindResource("e") as Ellipse).Fill?.ToString() ?? "null"));
MessageBox.Show("d: " + ((FindResource("d") as C).Prop?.ToString() ?? "null"));
MessageBox.Show("c: " + ((FindResource("c") as C).Prop?.ToString() ?? "null"));
MessageBox.Show("b: " + (FindResource("b") as SolidColorBrush).Color.ToString());
I get this result:
f: #00FFFFFF
e: null
d: null
c: #FFFF0000
b: #FFFF0000
i.e. only the last two are seemingly correct. What could be the reason for this?

Strange.
my:C has obviously no DataContext and can therefore not bind directly to anything.
Resources with DataContext do not inherit the resources owner's DataContext (Ellipses e and f)
SolidColorBrush "b" derive form System.Windows.Freezable which has a protected Field/Property called InheritanceContext which for "b" is set to the MainWindow. I think it has access to the Context.B through this reference and that's why "b" and "c" shows the right color.

I was curious about this, and so decided to play around with it a little. I am nowhere near an expert on XAML, but I wanted to share my findings in case it helps out at all.
I added the following XAML to a Grid on the page:
<Ellipse Fill="{StaticResource b}" />
<Ellipse Grid.Row="1" Fill="{Binding Source={StaticResource c}, Path=Prop}" />
<Ellipse Grid.Row="2" Fill="{Binding Source={StaticResource d}, Path=Prop}" />
<ContentControl Grid.Row="3" Content="{StaticResource e}" />
<ContentControl Grid.Row="4" Content="{StaticResource f}" />
Here is a screenshot of the results:
The following binding error appeared in the Output window, which I think explains why d is not filled:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement
or FrameworkContentElement for target element.
BindingExpression:Path=A; DataItem=null; target element is 'C'
(HashCode=60275915); target property is 'Prop' (type 'Brush')
It looks like it may be having trouble resolving the binding for d because it can't locate its position in the element hierarchy, and so can't resolve the DataContext.
I also noticed that if I commented the XAML out, the behavior was exactly the same as was posted in the question. Both e and d were null. Based on this, it seems like the usage of the resources may impact whether or not the binding can be resolved at runtime.
It would be great to hear from someone with more insight into the internal workings of how bindings function in resources.

Related

How to bind `Path.Data` in a resource dictionary in XAML (Silverlight)

It seems it is not possible to define geometry in a ResourceDictionary in Silverlight. So I'm using Path to store some geometries. Here is the Path.xaml file:
<ResourceDictionary ///some namespaces///>
<Path x:Key="path1" Data="//some geometry//" />
<Path x:Key="path2" Data="//some geometry//" />
<Path x:Key="path3" Data="//some geometry//" />
</ResourceDictionary>
Having reference this ResourceDictionary, I want to use these geometries somewhere in a UserControl:
<Path Data = "{Binding Source={StaticResouce path1}", Path=Data}"/>
But I'm getting the following error:
Value does not fall within the expected range.
How can I get those geometries in the XAML?
Correct you can't. I came across this problem and got round it by having the path strings as properties of a class and then adding that class as a resource. For example here is the class
public class IconPaths
{
public string CircleIcon
{
get { return "M50.5,4.7500001C25.232973,4.75 4.75,25.232973 4.7500001,50.5 4.75,75.767029 25.232973,96.25 50.5,96.25 75.767029,96.25 96.25,75.767029 96.25,50.5 96.25,25.232973 75.767029,4.75 50.5,4.7500001z M50.5,0C78.390381,0 101,22.609621 101,50.5 101,78.390381 78.390381,101 50.5,101 22.609621,101 0,78.390381 0,50.5 0,22.609621 22.609621,0 50.5,0z"; }
}
public string LogIcon
{
get { return "M16.6033,19.539C18.922133,19.539 20.042,21.777275 20.042,23.919949 20.042,26.367331 18.793436,28.397999 16.5877,28.397999 14.394864,28.397999 13.149,26.334829 13.149,24.032551 13.149,21.665472 14.301367,19.539 16.6033,19.539z M5.3724453,18.578932L5.3724453,29.357607 11.370038,29.357607 11.370038,28.189699 6.7645523,28.189699 6.7645523,18.578932z M28.522547,18.46693C24.908003,18.46693 22.700978,20.817846 22.685377,24.032669 22.685377,25.711081 23.260882,27.151291 24.189095,28.045797 25.244007,29.053604 26.587723,29.469608 28.217943,29.469608 29.67366,29.469608 30.905476,29.101805 31.529183,28.877802L31.529183,23.696165 27.97814,23.696165 27.97814,24.816576 30.169766,24.816576 30.169766,28.029497C29.848162,28.189699 29.225756,28.317898 28.314345,28.317898 25.803812,28.317898 24.156695,26.703287 24.156695,23.968267 24.156695,21.265749 25.867714,19.634937 28.490048,19.634937 29.57736,19.634937 30.297468,19.84334 30.872776,20.097841L31.20888,18.963034C30.745175,18.739132,29.770062,18.46693,28.522547,18.46693z M16.666903,18.40313C13.788068,18.40313 11.661542,20.641445 11.661542,24.064671 11.661542,27.326992 13.660466,29.534107 16.506901,29.534107 19.258234,29.534107 21.510861,27.567295 21.510861,23.856268 21.510861,20.657745 19.609839,18.40313 16.666903,18.40313z M14.433776,2.6588883C12.967757,2.6588886,11.773743,3.8522573,11.773743,5.319067L11.773743,15.361408 34.872822,15.361408C35.828533,15.361408,36.603447,16.136215,36.603447,17.092621L36.603447,30.392214C36.603447,31.347221,35.828533,32.121925,34.872822,32.121925L11.773743,32.121925 11.773743,37.347263C11.773743,38.814075,12.967757,40.007481,14.433776,40.007481L35.931637,40.007481C37.397755,40.007481,38.59177,38.814075,38.59177,37.347263L38.59177,12.690889 29.754463,12.690889C29.022553,12.690889,28.424946,12.092585,28.424946,11.36008L28.424946,2.6601186 28.424946,2.6588883z M14.433776,0L29.723061,0 41.251999,11.530681 41.251999,37.347263C41.251999,40.280281,38.866474,42.667,35.931637,42.667L14.433776,42.667C11.498941,42.667,9.1135704,40.280281,9.1135704,37.347263L9.1135704,32.121925 1.7293315,32.121925C0.7749116,32.121925,1.9777951E-07,31.347221,0,30.392214L0,17.092621C1.9777951E-07,16.136215,0.7749116,15.361408,1.7293315,15.361408L9.1135704,15.361408 9.1135704,5.319067C9.1135704,2.3854568,11.498941,0,14.433776,0z"; }
}
}
And here is it called in a resource dictionary
<LocalStyles:IconPaths x:Key="IconPaths"/>
And finaly used in a path
<Path Data="{Binding Source={StaticResource IconPaths}, Path=ArrowIcon}" Height="10" Stretch="Fill" Fill="DimGray" VerticalAlignment="Center" HorizontalAlignment="Center" RenderTransformOrigin="0.5,0.5">

PropertyChangedCallback not firing when respective property bound to ScrollBar

I have a rather simple UserControl which I would like to extend with the DependencyProperty. The relevant code of the control is as follows:
public partial class CompassControl : UserControl
{
public static readonly DependencyProperty AngleProperty =
DependencyProperty.Register("Angle", typeof(Double), typeof(CompassControl),
new FrameworkPropertyMetadata( 0.0, FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnAngleChanged)));
private static void OnAngleChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
((CompassControl)target).SetImageAngle((Double)e.NewValue);
}
public CompassControl( )
{
InitializeComponent( );
}
public Double Angle
{
get { return (Double)GetValue(AngleProperty); }
set { SetValue(AngleProperty, value); }
}
This control is being used on a simple form; the relevant XAML as follows:
<DockPanel DockPanel.Dock="Bottom">
<DockPanel>
<TextBlock DockPanel.Dock="Left"
TextAlignment="Center" FontWeight="Bold" FontSize="12"
Padding="0,4,0,0" HorizontalAlignment="Left"
Height="22" Width="60" Margin="10,0,0,0"
Text="{Binding ElementName=scrollBarAngle, Path=Value}">
</TextBlock>
<ScrollBar DockPanel.Dock="Left" Name="scrollBarAngle" Orientation="Horizontal"
Height="22" Margin="10,0"
Maximum="360.0" Minimum="0.0" SmallChange="1.0" Value="0.0"
ValueChanged="scrollBarAngle_ValueChanged" />
</DockPanel>
</DockPanel>
<ctl:CompassControl DockPanel.Dock="Top" Name="compassControl"
Margin="5" Width="Auto" Height="Auto"
Angle="{Binding ElementName=scrollBarAngle, Path=Value}"
/>
</DockPanel>
The "Text" property of the TextBox and the "Angle" property of my control are bound to the "Value" property of the ScrollBar using the following binding:
"{Binding ElementName=scrollBarAngle, Path=Value}"
When I scroll the ScrollBar, the Text field is updated as expected, but the Angle does not change - the OnAngleChanged callback is not being called!
However if I directly change the Angle property in the ScrollBar's ValueChanged event everything works fine - the property got changed and the respective callback fired:
private void scrollBarAngle_ValueChanged( object sender, RoutedPropertyChangedEventArgs<double> e )
{
compassControl.Angle = e.NewValue;
}
Please help resolve this issue!
Thank you,
--Alex
My apologies - the problem was not in the code, but in the environment! I had several copies of VS2013 open, the project was open in two of them. Anyway, after reading Clemens comment indicating that my problem is not reproducible, I closed all instances of VS, then started the fresh instance, opened the project - and everything worked fine!
Thank you!

Convert Shape into reusable Geometry in WPF

I am trying to convert a System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry object.
With the Geometry object, I am going to render it multiple times with a custom graph control depending on a set of data points. This requires that each instance of the Geometry object has a unique TranslateTransform object.
Now, I am approaching the issue in two different ways, but neither seems to be working correctly. My custom control uses the following code in order to draw the geometry:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry.Clone();
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
geo.Transform = translation;
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
I have also tried the following alternate code:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry;
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
dc.PushTransform(translation);
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
dc.Pop(); //Undo translation.
The difference is that the second snippet doesn't clone or modify the Shape.RenderedGeometry property.
Oddly enough, I occasionally can view the geometry used for the data points in the WPF designer. However, the behavior is inconsistent and difficult to figure out how to make the geometry always appear. Also, when I execute my application, the data points never appear with the specified geometry.
EDIT: I have figured out how to generate the appearance of the geometry. But this only works in design-mode. Execute these steps:
Rebuild project.
Go to MainWindow.xaml and click in the custom shape object so that the shape's properties load into Visual Studio's property window. Wait until the property window renders what the shape looks like.
Modify the data points collection or properties to see the geometry rendered properly.
Here is what I want the control to ultimately look like for now:
How can I convert a Shape object to a Geometry object for rendering multiple times?
Your help is tremendously appreciated!
Let me give the full context of my problem, as well as all necessary code to understanding how my control is set up. Hopefully, this might indicate what problems exist in my method of converting the Shape object to a Geometry object.
MainWindow.xaml
<Window x:Class="CustomControls.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Grid>
<local:LineGraph>
<local:LineGraph.DataPointShape>
<Ellipse Width="10" Height="10" Fill="Red" Stroke="Black" StrokeThickness="1" />
</local:LineGraph.DataPointShape>
<local:LineGraph.DataPoints>
<local:DataPoint X="10" Y="10"/>
<local:DataPoint X="20" Y="20"/>
<local:DataPoint X="30" Y="30"/>
<local:DataPoint X="40" Y="40"/>
</local:LineGraph.DataPoints>
</local:LineGraph>
</Grid>
DataPoint.cs
This class just has two DependencyProperties (X & Y) and it gives a notification when any of those properties are changed. This notification is used to trigger a re-render via UIElement.InvalidateVisual().
public class DataPoint : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
public static readonly DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
private static void DataPoint_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataPoint dp = (DataPoint)sender;
dp.RaisePropertyChanged(e.Property.Name);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, (double)value); }
}
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, (double)value); }
}
}
LineGraph.cs
This is the control. It contains the collection of data points and provides mechanisms for re-rendering the data points (useful for WPF designer). Of particular importance is the logic posted above which is inside of the UIElement.OnRender() method.
public class LineGraph : FrameworkElement
{
public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged));
public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection<DataPoint>), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection<DataPoint>), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged));
private static void DataPointShapeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
LineGraph g = (LineGraph)sender;
g.InvalidateVisual();
}
private static void DataPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{ //Collection referenced set or unset.
LineGraph g = (LineGraph)sender;
INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;
INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
if (oldValue != null)
oldValue.CollectionChanged -= g.DataPoints_CollectionChanged;
if (newValue != null)
newValue.CollectionChanged += g.DataPoints_CollectionChanged;
//Update the point visuals.
g.InvalidateVisual();
}
private void DataPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ //Collection changed (added/removed from).
if (e.OldItems != null)
foreach (INotifyPropertyChanged n in e.OldItems)
{
n.PropertyChanged -= DataPoint_PropertyChanged;
}
if (e.NewItems != null)
foreach (INotifyPropertyChanged n in e.NewItems)
{
n.PropertyChanged += DataPoint_PropertyChanged;
}
InvalidateVisual();
}
private void DataPoint_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Re-render the LineGraph when a DataPoint has a property that changes.
InvalidateVisual();
}
public Shape DataPointShape
{
get { return (Shape)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, (Shape)value); }
}
public ObservableCollection<DataPoint> DataPoints
{
get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty); }
set { SetValue(DataPointsProperty, (ObservableCollection<DataPoint>)value); }
}
public LineGraph()
{ //Provide instance-specific value for data point collection instead of a shared static instance.
SetCurrentValue(DataPointsProperty, new ObservableCollection<DataPoint>());
}
protected override void OnRender(DrawingContext dc)
{
if (DataPointShape != null)
{
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
foreach (DataPoint dp in DataPoints)
{
Geometry geo = DataPointShape.RenderedGeometry.Clone();
TranslateTransform translation = new TranslateTransform(dp.X, dp.Y);
geo.Transform = translation;
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
}
}
}
}
EDIT 2:In response to this answer by Peter Duniho, I would like to provide the alternate method to lying to Visual Studio in creating a custom control. For creating the custom control execute these steps:
Create folder in root of project named Themes
Create resource dictionary in Themes folder named Generic.xaml
Create a style in the resource dictionary for the control.
Apply the style from the control's C# code.
Generic.xamlHere is an example of for the SimpleGraph described by Peter.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Style TargetType="local:SimpleGraph" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Style.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/>
</Style.Resources>
<Style.Setters>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointFill}"
Stroke="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStroke}"
StrokeThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStrokeThickness}"
Data="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointGeometry}">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
Lastly, apply the style like so in the SimpleGraph constructor:
public SimpleGraph()
{
DefaultStyleKey = typeof(SimpleGraph);
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
I think that you are probably not approaching this in the best way. Based on the code you posted, it seems that you are trying to do manually things that WPF is reasonably good at handling automatically.
The main tricky part (at least for me…I'm hardly a WPF expert) is that you appear to want to use an actual Shape object as the template for your graph's data point graphics, and I'm not entirely sure of the best way to allow for that template to be replaced programmatically or declaratively without exposing the underlying transformation mechanic that controls the positioning on the graph.
So here's an example that ignores that particular aspect (I will comment on alternatives below), but which I believe otherwise serves your precise needs.
First, I create a custom ItemsControl class (in Visual Studio, I do this by lying and telling VS I want to add a UserControl, which gets me a XAML-based item in the project…I immediately replace "UserControl" with "ItemsControl" in both the .xaml and .xaml.cs files):
XAML:
<ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
mc:Ignorable="d"
x:Name="root"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5" />
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Data="{Binding ElementName=root, Path=DataPointGeometry}"
Fill="Red" Stroke="Black" StrokeThickness="1">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
C#:
public partial class SimpleGraph : ItemsControl
{
public Geometry DataPointGeometry
{
get { return (Geometry)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, value); }
}
public static DependencyProperty DataPointShapeProperty = DependencyProperty.Register(
"DataPointGeometry", typeof(Geometry), typeof(SimpleGraph));
public SimpleGraph()
{
InitializeComponent();
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
}
The key here is that I have an ItemsControl class with a default ItemTemplate that has a single Path object. That object's geometry is bound to the controls DataPointGeometry property, and its RenderTransform is bound to the data item's X and Y values as offsets for a translation transform.
A simple Canvas is used for the ItemsPanel, as I just need a place to draw things, without any other layout features. Finally, there is a resource defining a default geometry to use, in case the caller doesn't provide one.
And about that caller…
Here is a simple example of how one might use the above:
<Window x:Class="TestSO28332278SimpleGraphControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<PathGeometry x:Key="dataPointGeometry"
Figures="M 0.5000,0.0000
L 0.6176,0.3382
0.9755,0.3455
0.6902,0.5618
0.7939,0.9045
0.5000,0.7000
0.2061,0.9045
0.3098,0.5618
0.0245,0.3455
0.3824,0.3382 Z">
<PathGeometry.Transform>
<ScaleTransform ScaleX="20" ScaleY="20" />
</PathGeometry.Transform>
</PathGeometry>
</Window.Resources>
<Grid>
<Border Margin="3" BorderBrush="Black" BorderThickness="1">
<local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}">
<local:SimpleGraph.Items>
<local:DataPoint X="10" Y="10" />
<local:DataPoint X="25" Y="25" />
<local:DataPoint X="40" Y="40" />
<local:DataPoint X="55" Y="55" />
</local:SimpleGraph.Items>
</local:SimpleGraph>
</Border>
</Grid>
</Window>
In the above, the only truly interesting thing is that I declare a PathGeometry resource, and then bind that resource to the control's DataPointGeometry property. This allows the program to provide a custom geometry for the graph.
WPF handles the rest through implicit data binding and templating. If the values of any of the DataPoint objects change, or the data collection itself is modified, the graph will be updated automatically.
Here's what it looks like:
I will note that the above example only allows you to specify the geometry. The other shape attributes are hard-coded in the data template. This seems slightly different from what you asked to do. But note that you have a few alternatives here that should address your need without requiring the reintroduction of all the extra manual-binding/updating code in your example:
Simply add other properties, bound to the template Path object in a fashion similar to the DataPointGeometry property. E.g. DataPointFill, DataPointStroke, etc.
Go ahead and allow the user to specify a Shape object, and then use the properties of that object to populate specific properties bound to the properties of the template object. This is mainly a convenience to the caller; if anything, it's a bit of added complication in the graph control itself.
Go whole-hog and allow the user to specify a Shape object, which you then convert to a template by using XamlWriter to create some XAML for the object, add the necessary Transform element to the XAML and wrap it in a DataTemplate declaration (e.g. by loading the XAML as an in-memory DOM to modify the XAML), and then using XamlReader to then load the XAML as a template which you can then assign to the ItemTemplate property.
Option #3 seems the most complicated to me. So complicated in fact that I did not bother to prototype an example using it…I did a little research and it seems to me that it should work, but I admit that I did not verify for myself that it does. But it would certainly be the gold standard in terms of absolute flexibility for the caller.

WPF dynamically load xaml with validation rules

I am relatively new in WPF and I face a problem.
I have to implement a form that gets the UI(xaml) from the database (as also the data).
Each of these forms that will be created at runtime they will have different controls.
Although I disagree with this approach I have to follow my boss directions.
The problem is with the validation.
We decided to do it with Validation Rules.
So I tried to implemented the basic example with the AgeRangeRule.
<TextBox Name="textBox1" Width="50" FontSize="15"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
Style="{StaticResource textBoxInError}"
Grid.Row="1" Grid.Column="1" Margin="2">
<TextBox.Text>
<Binding Path="Age" Source="{StaticResource ods}"
UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<c:AgeRangeRule Min="21" Max="130"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
The error that I get when I load the xaml is
Additional information: 'Cannot create unknown type '{clr-namespace:WpfDynamicTest1}AgeRangeRule'.'
And is in this line:
<c:AgeRangeRule Min="21" Max="130"/>
Note: c is defined as:
xmlns:c="clr-namespace:WpfDynamicTest1"
How can I overcome this error?
I faced similar errors with the ControlTemplate and Style for the errors but I moved them to the Application.xaml and my problems solved.
Can I do something similar with the reference to the class?
Edit: Additional Info:
How I load the xaml:
The "cell" form has these properties:
Public Property FormId() As Integer
Get
Return miFormId
End Get
Set(ByVal value As Integer)
miFormId = value
FormCharacteristics(value)
End Set
End Property
Public Property UI() As String
Get
Return msUI
End Get
Set(ByVal value As String)
msUI = value
Dim rootObject As DependencyObject = XamlReader.Parse(value)
Me.Content = rootObject
End Set
End Property
So when I call the form I do this:
Dim winD As New winDynamic
winD.FormId = 4
winD.Show()
The FormCharacteristics fills msUI and UI is loaded.
Though not sure if you search through some of the following links but i hope they could be of help to you:
Compile/Execute XAML during program runtime
WPF – dynamically compile and run event handlers within loose XAML using CodeDom
Loading XAML at runtime?
Error: 'Cannot create unknown type '{clr-namespace:NameSpace.Properties}Settings'.'
EDIT
Based on the links above, assuming you are using XamlReader, I created a sample and its working fine. In this case, the reason I found is, the XAML Parser need the ParserContext to map the namespaces to bind the required types at run time.
Xaml (Dynamic usercontrol to load)
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300"
xmlns:c="clr-namespace:WpfApplication1">
<UserControl.Resources>
<c:MyDataSource x:Key="ods"/>
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="20">!</TextBlock>
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel>
<TextBox Name="textBox1" Width="50" FontSize="15"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
Style="{StaticResource textBoxInError}"
Grid.Row="1" Grid.Column="1" Margin="2">
<TextBox.Text>
<Binding Path="Age" Source="{StaticResource ods}"
UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<c:AgeRangeRule Min="21" Max="130"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="btnDynamic" Width="150" Height="30" Content="Click Me"/>
</StackPanel>
</UserControl>
Code behind (C#)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadXAML();
}
public void LoadXAML()
{
try
{
using (StreamReader xamlStream = new StreamReader(#"C:\WpfApplication1\WpfApplication1\DynamicWindow.xaml"))
{
var context = new ParserContext();
context.XamlTypeMapper = new XamlTypeMapper(new string[] { });
context.XmlnsDictionary.Add("c", "clr-namespace:WpfApplication1");
context.XamlTypeMapper.AddMappingProcessingInstruction("clr-namespace:WpfApplication1", "WpfApplication1", "WpfApplication1");
string xamlString = xamlStream .ReadToEnd();
DependencyObject rootObject = XamlReader.Parse(xamlString, context) as DependencyObject;
cntControl.Content = rootObject; //cntControl is a content control I placed inside MainWindow
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message.ToString());
}
}
}
Note
For the Binding Validation., I used same MSDN code you provided.
Also since I am not with the VB.NET HAT now, I choose C# for the code behind!! Though the code is simple enough.
Your AngeRangeRule should derive from ValidationRule.
public class AgeRangeRule : ValidationRule
{
....
}
And you have to override ValidationResult member:
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
// Cast value object and check if it is valid
return new ValidationResult(...,...);
}

How can I display a different ToolTip based on the DataContext DataType in Wpf?

I have an abstract UserControl that I want to show a ToolTip on. This ToolTip should be different based on the Type of the DataContext which is defined in the derived UserControls.
Is there a way to define a different ToolTip for each type in the base class? If not, how can I set this ToolTip in the derived UserControl?
Here is how I thought I would go:
<UserControl ...
<UserControl.ToolTip>
<DataTemplate DataType="{x:Type Library:Event}">
<StackPanel>
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock>
<TextBlock.Text>
<Binding Path="Kp" StringFormat="{}Kp: {0}m" />
</TextBlock.Text>
</TextBlock>
</StackPanel>
</DataTemplate>
</UserControl.ToolTip>
</UserControl>
Couldn't you author a custom ValueConverter that returns the information you'd like to display for the type?
You could 'fancy this up' a bit to allow the converter to accept data templates like you're suggesting, but this will totally enable your scenario.
First, create the value converter. Pardon my quick code:
public class ToolTipConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
UIElement tip = null;
if (value != null)
{
// Value is the data context
Type t = value.GetType();
string fancyName = "Unknown (" + t.ToString() + ")";
// Can use IsInstanceOf, strings, you name it to do this part...
if (t.ToString().Contains("Person"))
{
fancyName = "My custom person type";
};
// Could create any visual tree here for the tooltip child
TextBlock tb = new TextBlock
{
Text = fancyName
};
tip = tb;
}
return tip;
}
public object ConvertBack(object o, Type t, object o2, CultureInfo ci)
{
return null;
}
}
Then instantiate it in your user control's resources (I defined the xmlns "local" to be this namespace and assembly):
<UserControl.Resources>
<local:ToolTipConverter x:Key="toolTipConverter" />
</UserControl.Resources>
And make sure the root visual of your user control binds its ToolTip property:
<Grid
ToolTip="{Binding Converter={StaticResource toolTipConverter}}"
Background="Blue">
<!-- stuff goes here -->
</Grid>
Although it's a really old post, I'll still post my answer, as I was facing the same problem today. Basically I ended up with putting all my tooltip templates into resourses, like the author of the question did. For this really to work there was a missing binding for the tooltip content and a resources section. With these in place, temlates do actually get applied.
<UserControl ...
<UserControl.ToolTip>
<Tooltip Content="{Binding}">
<Tooltip.Resources>
<DataTemplate DataType="{x:Type Type1}">
...
</DataTemplate>
<DataTemplate DataType="{x:Type Type2}">
...
</DataTemplate>
</Tooltip.Resources>
</Tooltip>
</UserControl>

Categories