I would like to implement a "NullText" behavior for a TextBlock that is bound to a property in a ViewModel. When that property in the ViewModel is null or empty, I would like to display gray italic text something like "No Data". I'd like this to follow MVVM pattern but I am lost...
Update
So after playing around with the solution James Webster suggested, I got it to work like this...
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<c:NullOrEmptyValueConverter x:Key="NullOrEmptyValueConverter" Text="(No Data)"/>
</UserControl.Resources>
...
<TextBlock Name="SerialNumberTextBlock" Text="{Binding Path=SerialNumber, Converter={StaticResource NullOrEmptyValueConverter}}">
<TextBlock.Resources>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SerialNumberTextBlock, Path=Text}" Value="(No Data)">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Resources>
</TextBlock>
I'd recommend implementing an IValueConverter; if the source value is not null or empty, then pass it through to the TextBlock. If the source value is null or empty, then render your chosen text.
public class NullValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string str = (string)value;
if (str.IsNullOrWhitespace())
{
return "No Data";
}
return str;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
... //An empty implementation I expect...
}
}
However I have just realised that you want to set the style as well... hmmm, probably a DataTrigger that sets the style if the value is 'No Data' required I expect;
<TextBlock Text="{Binding Path=SomeProperty, Converter={StaticResource keyToNullValueConverter}">
<TextBlock.Triggers>
<DataTrigger Binding="{Binding Path=Text}" Value="No Data">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
</TextBlock.Triggers>
</TextBlock>
Something along those lines might work.
I think you don't need to create Converter Class, you can simply write your style code like this.
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SerialNumberTextBlock, Path=Text}" Value="{x:Null}">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=SerialNumberTextBlock, Path=Text}" Value="{x:Static System:String.Empty}">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
</Style.Triggers>
</Style>
Note :- You need to include the system namespace as
xmlns:System="clr-namespace:System;assembly=mscorlib"
You could try to bind to a property that looks thus
private string _textBlockText;
public string TextBlockText
{
get
{
if(string.IsNullOrEmpty(_textBlockText))
{
return "No Data";
}
return _textBlockText;
}
set
{
_textBlockText = value;
}
}
and then use the XAML that James has mentioned for styling. Saves the need for a converter.
Very late to the party, but here is my answer.
<TextBox >
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding MyText, TargetNullValue=No data}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyText}" Value="{x:Null}">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<DataTrigger Binding="{Binding MyText}" Value="{x:Static System:String.Empty}">
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Text" Value="{Binding MyText}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
My answer assumes that you want the default text to disappear on focus, which is more in line how inputs with default text usually behave. Also this works only for null values. If you need a check for string empty or additional logic, you could instead use a converter like in other provided answers. The key idea is, that you remove null value text or converter in the binding when control gains focus and thus only display the default text when control has no focus. This also prevents the default value flowing back to your view model.
DataTriggers part for font style was kindly borrowed from pchajer's answer.
Related
I set up a style for a label which should be triggered if the value of the bound filed in the underlying DataTable is greater than zero:
<c:Groesser0BooleanValueConverter x:Key="G0" />
<Style x:Key="DashboardProzent" TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource G0}}" Value="{x:Null}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
The label itself is set up this way (a DataTemplate in a resource Dictionary):
<Label Content="{Binding percentCol}" Style="{StaticResource DashboardPrzoent}" Grid.Row="0" Grid.Column="2"/>
The converter looks like this:
public class Groesser0BooleanValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (System.Convert.ToInt32(value) > 0)
{
return true;
}
else
{
return false;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
If a run the whole thing, i get the error in the Convert method of the Groesser0BooleanValueConverter class:
Unable to cast object of type 'System.Data.DataRowView' to type
'System.IConvertible'
If I check the parameter of the Convert method of the Converter, it shows that a System.Data.DataRowView has been passed, instead of the value of the percentCol field, which I would have expected. How can I get the Label to pass the value instead of the whole row?
Update:
If I set the Label to
<Label Content="{Binding Path=percentCol}" Style="{StaticResource DashboardPrzoent}" Grid.Row="0" Grid.Column="2"/>
the error still occurs. If I disable the trigger, the value is shown (even without the Path= segment.
I don't want to state the field name in the style segment, as I would like to use it for other values to.
First I think the main problem is due to your Binding in the DataTrigger
<DataTrigger Binding="{Binding Converter={StaticResource G0}}" Value="{x:Null}">
This will pass on the direct DataContext of your Label to your Converter, which is obviously not what you want.
You should have:
<DataTrigger Binding="{Binding Converter={StaticResource G0}, Path=percentCol}" Value="False">
I also corrected the Value part, since your 'Converter' only sends back True/False. If you want to change back to Value="{x:Null}", you will have to modify your converter.
I would also advise to be safer in your Converter, otherwise the explicit conversion System.Convert.ToInt32(value) may (will...) raise an error.
I wanted to set the Binding Path and the Binding Converter in two different locations, for example a DataTemplate and a Style file.
This can be done by using {Binding Content, RelativeSource={RelativeSource Self}, Converter={StaticResource converterName}} in the Style and setting the Binding Path as usual in the DataTemplate
Converter
<Style x:Key="DashboardPrzoent" TargetType="{x:Type Label}">
...
<Style.Triggers>
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}, Converter={StaticResource G0}}" Value="True">
<Setter Property="Foreground" Value="{StaticResource Gruen}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}, Converter={StaticResource G0}}" Value="False">
<Setter Property="Foreground" Value="{StaticResource Rot}"/>
</DataTrigger>
</Style.Triggers>
</Style>
DataTemplate
<Label Content="{Binding Path=percentCol}" ... />
I have a combobox that has the period of the day as its items. I am trying to make the isSelected item based on the period on the day through the use of a converter but for some reason my code won't work.
Below is my xaml:
<Window.Resources>
<staticData:SelectedPeriodConverter x:Key="SelectedPeriodConverter"/>
</Window.Resources>
<ComboBox Grid.Column="4" Margin="0,7" Width="100" HorizontalAlignment="Right" Name="PeriodPicker" VerticalAlignment="Top" Height="25" SelectedItem="PM">
<ComboBoxItem>AM</ComboBoxItem>
<ComboBoxItem>PM</ComboBoxItem>
<ComboBox.ItemContainerStyle>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Content, RelativeSource={RelativeSource Self}, Converter={StaticResource SelectedPeriodConverter}}" Value="True">
<Setter Property="ComboBoxItem.IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
and my c# code for the converter is as follows:
public class SelectedPeriodConverter : IValueConverter
{
public object Convert(object values, Type targetType, object parameter, CultureInfo culture)
{
string test = values.ToString();
if (test == DateTime.Now.ToString("tt"))
{
return true;
}
else
{
return false;
}
}
public object ConvertBack(object value, Type targetTypes, object parameter, CultureInfo culture)
{
return value;
}
}
The weird thing is is if I change isSelected to IsEnable it will trigger but otherwise it won't.
Another way I tried was to have the styling in the Windows resources.
This worked if I targetted comboboxitem but would not work if I added an x:class and used ItemContainerStyle to isolate the trigger to one combobox as I did not want it working on all the comboboxes I had in my form.
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Content, RelativeSource={RelativeSource Self}, Converter={StaticResource SelectedPeriodConverter}}" Value="True">
<Setter Property="IsSelected" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
<ComboBox Grid.Column="4" Margin="0,7" Width="100" HorizontalAlignment="Right" Name="PeriodPicker" VerticalAlignment="Top" Height="25" SelectedItem="PM">
<ComboBoxItem>AM</ComboBoxItem>
<ComboBoxItem>PM</ComboBoxItem>
</ComboBox>
Does anyone know how I can get this to work?
thanks Callum
The fix was quiet simple.
Inserting the second code inside ComboBox.Resources tags instead of Windows.Resources tags worked for me like so:
<ComboBox.Resources>
<staticData:SelectedPeriodConverter x:Key="SelectedPeriodConverter"/>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Content, RelativeSource={RelativeSource Self}, Converter={StaticResource SelectedPeriodConverter}}" Value="True">
<Setter Property="IsSelected" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
thanks Callum
I have a TextBox with a style that has a DataTrigger which changes the text, like this:
<Grid>
<TextBlock Text="Foo">
<TextBlock.Style>
<Style BasedOn="{StaticResource TextStyle}" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding MyBool}" Value="True">
<Setter Property="Text" Value="Bar"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
But it's not working, the text never changes to "Bar". I have tested using another TextBlock with Text="{Binding MyBool}" and this text changes from "False" to "True". Snoop reveals no errors that I can see and there is nothing in the output.
This question may seem like a duplicate of WPF Trigger binding to MVVM property, but my code does not seem different from the accepted answer there (http://www.thejoyofcode.com/Help_Why_cant_I_use_DataTriggers_with_controls_in_WPF.aspx, section "Using a style") in any relevant way. And using a DataTemplate as suggested in the actual answer seems wrong since I only want this to apply to a single TextBlock, but if it is correct, I'm not sure how to write a DataTemplate for this...
EDIT:
This is what the property I'm binding to looks like:
public bool MyBool
{
get { return _myBool; }
set
{
if (_myBool== value)
return;
_myBool= value;
NotifyPropertyChanged();
}
}
private bool _myBool;
Dependency Properties can be set from many different places; inline, animations, coercion, triggers, etc. As such a Dependency Property Value Precedence list was created and this dictates which changes override which other changes. Because of this order of precedence, we can't use a Trigger to update a property that is explicitly set inline in your XAML. Try this instead:
<Grid>
<TextBlock>
<TextBlock.Style>
<Style BasedOn="{StaticResource TextStyle}" TargetType="TextBlock">
<!-- define your default value here -->
<Setter Property="Text" Value="Foo" />
<Style.Triggers>
<DataTrigger Binding="{Binding MyBool}" Value="True">
<!-- define your triggered value here -->
<Setter Property="Text" Value="Bar" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
I've got a button with a fixed background image and would like to show a small overlay image on top of it. Which overlay image to chose depends on a dependency property (LapCounterPingStatus) of the according viewmodel.
This is what I got so far:
<Button>
<Grid>
<Image Stretch="None"> <!-- Background Image -->
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Source" Value="/Images/Pingn.png"/>
</Style>
</Image.Style>
</Image>
<Image Stretch="None" Panel.ZIndex="1"> <!-- Small Overlay Image -->
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=LapCounterPingStatus}" Value="PingStatus.PING_UNKNOWN">
<Setter Property="Source" Value="/Images/RefreshOverlayn.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=LapCounterPingStatus}" Value="PingStatus.PING_FAILURE">
<Setter Property="Source" Value="/Images/ErrorOverlayn.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=LapCounterPingStatus}" Value="PingStatus.PING_SUCCESS">
<Setter Property="Source" Value="/Images/CheckmarkOverlayn.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Grid>
</Button>
Relevant parts of my viewmodel
public class ConfigurationViewModel
{
public enum PingStatus { PING_UNKNOWN, PING_SUCCESS, PING_FAILURE };
public PingStatus LapCounterPingStatus
{
get { return _lapCounterPingStatus; }
set
{
_lapCounterPingStatus = value;
RaisePropertyChanged(LapCounterPingStatusPropertyName);
}
}
}
Right now, no overlay image at all is displayed. What could be wrong?
UPDATE
Trace window of my IDE is showing System.ArgumentException and System.FormatException.
Could the problem source be a unknown type of enumeration PingStatus im the XAML?
You need 2 things to get this working:
1 - Add an xmlns reference in the root element of your XAML file, to the namespace where your Enum is defined:
<UserControl ...
xmlns:my="clr-namespace:YourEnumNamespace;assembly=YourAssembly">
2 - in the Value property of the DataTrigger, use the {x:Static} form:
<DataTrigger Binding="{Binding Path=LapCounterPingStatus}" Value="{x:Static my:PingStatus.PING_UNKNOWN}">
Notice that the Enum type must be prefixed with the xmlns prefix you defined above.
Edit:
If your Enum is declared inside a class you need to use the syntax:
{x:Static namespace:ClassName+EnumName.EnumValue}
for example:
{x:Static my:ConfigurationViewModel+PingStatus.PING_UNKNOWN}
Complete worked example for WPF + MVVM.
Tested on MSVC 2017.
In the view:
<TextBlock Text="Some text to be colored by an enum">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding StatusIcon}" Value="{x:Static my:StatusIcon.Warning}">
<Setter Property="Foreground" Value="Yellow"/>
</DataTrigger>
<DataTrigger Binding="{Binding StatusIcon}" Value="{x:Static my:StatusIcon.Error}">
<Setter Property="Foreground" Value="Red}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
If using ReSharper, and if the DataContext is set up properly, there will be intellisense when you hit the . after StatusIcon, i.e. it will show the properties of the enum which are Debug, Info, Warning or Error.
If using ReSharper, it will suggest the following update to the namespace in the header for the XAML file(its
good like that):
xmlns:my="clr-namespace:Class.Path.MyViewModel;assembly=MyAssembly"
And the VieModel:
public enum StatusIcon
{
Debug,
Info,
Warning,
Error
}
public class MyViewModel
{
public StatusIcon StatusIcon { get; }
}
We also use Fody for automated binding.
You can simply set enum value as DataTrigger Value... Tested on MSVC 2017.
<TextBlock Text="Some text to be colored by an enum">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding StatusIcon}" Value="Warning">
<Setter Property="Foreground" Value="Yellow"/>
</DataTrigger>
<DataTrigger Binding="{Binding StatusIcon}" Value="Error">
<Setter Property="Foreground" Value="Red}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
I have a collection of objects bound to a hierarchical data template, each of my objects have a property on them (lets call it Property "A") that is of a certain type. This type varies among each of the objects.
If the data template contains an image and some text, what would be the best way to change the image that is displayed in the template based on the type of property "A".
I know I could just stick this into a converter and do the binding translation manually in code, but with all the binding facilities available in WPF, I think theres probably a better way.
It's pretty simple to do this within your data template, if you create local data templates and use a ContentPresenter. This template presents objects of type MyObject, displaying an image whose source is determined by the type of the A property next to a TextBlock that displays the content of the Text property:
<DataTemplate DataType="{x:Type MyObject}">
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<DataTemplate DataType="{x:Type Thing1}">
<Image Source="thing1.png"/>
</DataTemplate>
<DataTemplate DataType="{x:Type Thing2}">
<Image Source="thing2.png"/>
</DataTemplate>
</StackPanel.Resources>
<ContentPresenter Content="{Binding A}"/>
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</DataTemplate>
If you want to use styles to do this instead, you're going to run into a problem, because data triggers want to look at property values, and the type of the A property is not, itself, exposed as a property.
Unless, of course, you implement one:
public Type AType { get { return A.GetType(); } }
(You'll also need to raise PropertyChanged for AType when the value of A changes.) Once you've done this, you should be able to implement a data trigger in a style, e.g.:
<Style TargetType="Image">
<Setter Property="Source" Value="default.png"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AType}" Value="{x:Type Thing1}">
<Setter Property="Source" Value="thing1.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding AType}" Value="{x:Type Thing2}">
<Setter Property="Source" Value="thing2.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
I think You can do that with triggers.
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Source" Value="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding TheProperty}" Value="TheValue">
<Setter Property="Source" Value="NewPath"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
DataTemplateSelector doesn't seem to be a good choice here since you have the same template for all values of A.
Use DataTriggers:
<DataTemplate>
<StackPanel>
<Image x:Name="image" />
<TextBlock>Your text</TextBlock>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=A}" Value="ValueToCheck1">
<DataTrigger.Setters>
<Setter Property="Source" Value="Image1.png" TargetName="image" />
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=A}" Value="ValueToCheck2">
<DataTrigger.Setters>
<Setter Property="Source" Value="Image2.png" TargetName="image" />
</DataTrigger.Setters>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Haven't tested it, but the idea is like that.