I have implemented a gauge control as a custom control in WPF. It is working very well, but I have hit an issue which is causing me to doubt whether it is structured it correctly.
Using different control templates, I can give the gauge radically different styles, for example like a speedometer, a voltmeter, a thermometer or an oil level.
The top level gauge control contains two collections, of Ranges and Pointers.
A Range defines a colored area on the scale. This is a simple object with a few properties.
A Pointer, fairly obviously, defines a pointer on the gauge. A guage can have multiple pointers, and the pointers can have differnt styles, for example a needle, a colored block or a slider (which the user can drag).
This is an example configuration:
<gauge:Gauge MinValue="0"
MaxValue="350"
MajorDivisions="7"
MinorDivisions="5"
Caption="Bar"
Background="White"
LabelOrientation="Horizontal"
BorderThickness="3"
StartAngle="-180"
EndAngle="135"
MinorTickMarkColor="Black"
MajorTickMarkColor="Black"
LabelsOnTicks="Even"
LabelRadiusRelative="0.65"
Template="{StaticResource DefaultRotaryGauge}" >
<gauge:Gauge.Ranges>
<gauge:Range MinValue="200" MaxValue="350" Color="Green"/>
</gauge:Gauge.Ranges>
<gauge:Gauge.Pointers>
<gauge:Pointer Position="{Binding Position1}" ShowNumericValue="Visible" Color="LightGray"/>
<gauge:Pointer Position="{Binding Position2}" ShowNumericValue="Visible" Color="Red"/>
</gauge:Gauge.Pointers>
<gauge:Gauge.LabelStyle>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="10"/>
<Setter Property="Foreground" Value="Black"/>
</Style>
</gauge:Gauge.LabelStyle>
</gauge:Gauge>
As shown above, the pointer position must work with binding, so it must be a dependency property and it must - as far as I understand - be in the visual tree.
For this purpose, the pointer class is defined as a control.
The control template for the gauge uses an ItemsControl to include the pointers. Each pointer has its own control template, but because there are different styes of pointer, I use a trigger to select one of several different templates for a pointer.
For example, my control template for a thermometer supports two pointer styles, which are selected as follows.
<ItemsControl ItemsSource="{TemplateBinding Pointers}"
Grid.Row="2"
Grid.Column="3"
ItemsPanel="{StaticResource GridTemplate}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="local:Pointer">
<Setter Property="Control.Template" Value="{StaticResource ThermometerPointer}"/>
<Style.Triggers>
<Trigger Property="DisplayStyle" Value="Block">
<Setter Property="Control.Template" Value="{StaticResource ThermometerBlock}"/>
</Trigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
So far so good.
Now I have had a request, to change the colour of the liquid in the thermometer, when the value passes a given threshold. When I tried to implement this with a DataTrigger,
<gauge:Pointer Position="{Binding Position1}" DisplayStyle="Block">
<gauge:Pointer.Style>
<Style TargetType="gauge:Pointer">
<Setter Property="Color" Value="Red"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource GreaterThanConverter}">
<Binding Path="Position1"/>
<Binding Source="200.0"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Color" Value="Blue"/>
</DataTrigger>
</Style.Triggers>
</Style>
</gauge:Pointer.Style>
</gauge:Pointer>
the pointer stopped working entirely, because it lost the connection to its control template.
I was able to fix it by specifying the Control.Template in addition to the DataTrigger.
<gauge:Pointer Position="{Binding Position1}" DisplayStyle="Block">
<gauge:Pointer.Style>
<Style TargetType="gauge:Pointer">
<Setter Property="Control.Template" Value="{StaticResource ThermometerBlock}"/>
<Setter Property="Color" Value="Red"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource GreaterThanConverter}">
<Binding Path="Position1"/>
<Binding Source="200.0"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Color" Value="Blue"/>
</DataTrigger>
</Style.Triggers>
</Style>
</gauge:Pointer.Style>
</gauge:Pointer>
This works, but I'm not happy with it. The user of the guage control cannot know this detail about the internal impolementation.
I think it is probably bad practice for the control template of the gauge control to define a Style for the pointer. My problem is I can't figure out another way to select different control templates for the pointer.
I thought that I might be able to select a control template using a converter (converting my DisplayStyle property to a ControlTemplate), but I can see where I would apply the convertor.
My only other idea is to get rid of the DisplayStyle property and instead define multiple pointer classes which derive from a common base pointer class. I could then define separate control templates for each derived pointer class.
I have several questions.
Firstly, am I doing this right?
Does it even make sense to define the pointer as a control, with its own control template?
Secondly, is there a way to select the control template for the pointer, without requiring a style definition?
If not, is the idea with derived classes the way to go?
Related
I'm struggling to achieve string interpolation in Setter of the TextBlock in XAML.
I have a string with a {0} inside and want this "tag" to be replaced by some text. I'd use Multibinding and break my message into parts, but my app should be translated to other languages. For example, I have message like "Das Verzeichnis {0} wurde nicht gefunden" which due to semantics of different languages may look different, for example "The following path could nont been found: {0}". Sure, in this case I still can paraphrase it and then break into 3 parts to give a proper StringFormat to the Multibinding, but there is no guarantee it will work, let's say in Japanese. I also can't use multiple Runs, since Setter only receives one value.
Is there a way to get something similar to string.Format() in XAML?
UPD: Code:
<DataTrigger Binding="{Binding State}" Value="{x:Static vm:StatesEnum.UnknownError}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Text" Value="{Binding to resource string}" />
</DataTrigger>
UPD2: I wrote a simple converter, that makes all that stuff with string formatting. However, Binding seems to be tricky. XAML code below illustrates my problem. I need to bind to the whole DataContext of it, because State from the DataTrigger (first listing) is not sufficient for the text I want, I need some other fields from the bound object.
<Style x:Key="ImageErrorTextStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Text" Value="{Binding Path=???, Converter={StaticResource ImageToError}}" />
</Style>
However, as I found out, use of converter does not allow use of {Binding} (without Path=some_field). Leaving Path empty is allowed, but in this case Convert is only called once (probably creation or initialization of DataContext). The problem is, that the DataContext is declared in code behind and lies in collection, which is not bound to anything, so I have to apply this converter either in code behind or via setters in style. How can I bind Converter to the {Binding} or maybe there is another way of accessing the whole bound object?
Maybe you can try this :)
<DataTrigger Binding="{Binding State}" Value="{x:Static vm:StatesEnum.UnknownError}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Text">
<Setter.Value>
<MultiBinding StringFormat="{}{0}{1}">
<Binding Path="" Converter=""/>
<Binding Path="" Converter=""/>
</MultiBinding>
</Setter.Value>
</Setter>
</DataTrigger>
I don't understand why WPF allows me to write both
<Grid>
<Grid.Triggers>
<DataTrigger Binding="{Binding HasNeverBeenSeen}" Value="true">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Grid.Triggers>
</Grid>
and
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasNeverBeenSeen}" Value="true">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
but only the second seems to work. Why is there a Triggers tag to Grid element if we must use a Style?
Thanks
Short answer to your question is because this is how it is designed by WPF team.
FrameworkElement.Triggers can only have EventTriggers although property is collection of TriggerBase. It's also clearly stated on MSDN page:
Note that the collection of triggers established on an element only
supports EventTrigger, not property triggers (Trigger). If you require
property triggers, you must place these within a style or template and
then assign that style or template to the element either directly
through the Style property, or indirectly through an implicit style
reference.
I have this named style
<Style x:Key="validationSupport" TargetType="{x:Type Control}">
<Setter Property="Margin" Value="5,2,14,2" />
...OMISSIS...
<Style.Triggers>
...OMISSIS...
<DataTrigger Binding="{Binding DataContext.ActiveWorkspace.Editable, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
I use it extensively for TextBoxes, ComboBoxes, DatePickers etc, so I used as TargetType a super class for all these elements, Control.
Now I would like to differentiate the setter inside the dataTrigger using specific properties that 'Control' doesn't have. It seems I have to create different styles with different names,each for every targetType I want to differentiate, but that way I have to change the style name inside all elements which use it. Is there a smarter way to achieve that goal ? I don't want want to go and modify every xaml file I have.
Update after first answer
I have tried to put the following setters inside the datatrigger:
<Setter Property="Background" Value="#FFECECF8" />
<Setter Property="CheckBox.IsEnabled" Value="False" />
<Setter Property="DatePicker.IsEnabled" Value="False" />
<Setter Property="ComboBox.IsEnabled" Value="False" />
<Setter Property="TextBox.IsReadOnly" Value="True" />
Unfortunately the tests gave odd results. The IsEnabled property is set for TextBoxes too despite the prefix should limit its application to CheckBoxes, DatePickers and ComboBoxes.
My final need was to make some control contents unchangeable avoiding the difficult to read colors associated with disabled controls. From previous researches I understood that changing the colors for a 'disabled' control is not an easy task and involves the redefinition of the control template. So I thought to apply a combination of IsReadOnly and Background, but it is not applicable for the above problem. In fact CheckBoxes, DatePickers and ComboBoxes can only be made unchangeable using the IsEnabled property.
Am I missing something ?
There is a way, but I have to warn you - this is far from best-practice and should be avoided
WPF allows you to use desired type as a prefix for the property. That way, if you apply the style to a control that doesn't inherit from the prefixed type - the setter is ignored.
<Style x:Key="validationSupport" TargetType="{x:Type Control}">
<Setter Property="Margin" Value="5,2,14,2" />
...OMISSIS...
<Style.Triggers>
...OMISSIS...
<DataTrigger Binding="{Binding DataContext.ActiveWorkspace.Editable, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Button.Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
[Test this extensively, since I suspect that it might create memory leaks.]
I am sure this has been asked before, but I haven't had an easy time figuring out how to phrase the query.
I have this style;
<SolidColorBrush x:Key="SemiTransparentRedBrushKey">#F0FF0000</SolidColorBrush>
<Style x:Key="TextBoxEmptyError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text.Length}" Value="0">
<Setter Property="BorderBrush" Value="{StaticResource ResourceKey=SemiTransparentRedBrushKey}"/>
</DataTrigger>
</Style.Triggers>
</Style>
That I can apply to Textboxes to have a red border when they are empty. Its great, I can just add Style="{StaticResource TextBoxEmptyError}" to the Control Tag. But what if I want to apply this style with a trigger, so that the control only used it under certain conditions (like a binding being true)? Something like:
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ApprovedRequired}" Value="True">
<Setter Property="Style" Value="{StaticResource TextBoxEmptyError}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
This code throws an exception though {"Style object is not allowed to affect the Style property of the object to which it applies."}
Can something like this be done?
Edit: If this cannot be done with a Style trigger because it would overwrite itself, is there another way to Conditionally apply a resource style?
Edit: I can change the question title if there is a more proper term for this action.
Styles cannot be set from a Setter within the Style, because then essentially the first Style would never exist at all.
Since you're looking for a Validation style, I would recommend looking into Validation.ErrorTemplate, although if that doesn't work you can change your trigger so it modifies specific properties such as BorderBrush instead of the Style property
i would think of using a Template with a TemplateTrigger and there you can change the style to what ever you like based on what ever condition
I have a page with several controls. The controls are bound to display values which they get from the page's DataContext. What I would like to do is display another look of the page should the DataContext be null. In some cases the controls of the page should display differently if "their" property is set or not.
Is is possible to create a binding to see if the DataContext is set?
What I did as a workaround was to add a IsDataContextSet property to the page and the specify a binding like:
Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}, Path=IsDataContextSet}" Value="false"
This works as I expect but I have a feeling that their is more elegant way to do this. Or at least or more WPFish way.
Given the scenario you describe, I would set the properties with a style and a data trigger. The data trigger would use the default binding which is the data context.
An example might look like this:
<Border>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background"
Value="Orange" />
<Style.Triggers>
<DataTrigger Binding="{Binding}"
Value="{x:Null}">
<Setter Property="Background"
Value="Yellow" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
The border will be orange unless the data context is null, in which case the background is yellow.