I want to activate a trigger if the bound items property ID is equal to a property in my custom control.
But i can't use bindings in my triggers!
How would i do this?
This should work:
<DataTrigger
Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{x:Static local:EqualityConverter.Instance}">
<Binding
Path="BoundProperty" />
<Binding
ElementName="MockCustomControl"
Path="Text" />
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.Setters>
<Setter
TargetName=" ... "
Property=" ... "
Value=" ... " />
...
...
</DataTrigger.Setters>
</DataTrigger>
And the converter may be something similar to this:
public class EqualityConverter : IMultiValueConverter
{
public static readonly EqualityConverter Instance = new EqualityConverter();
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null || values.Length == 0) return false;
for (int i = 1; i < values.Length; i++)
if (!values[i].Equals(values[0])) return false;
return true;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
Note that I've used a generic convrter that can campare n values, but you can use a simple one as per your needs.
Hope this helps.
DataTrigger's value property doesn't take bindings?
<DataTrigger Binding="{Binding BoundPropertyId}" Value="{Binding ElementName=Mine, Path=Property}" />
If that doesn't work, you might need to hook onto an event and process it yourself in the code-behind.
Related
Can I pass an index number into this list (SomeList)?
FontSize="{Binding FontSize, Source={x:Static ut:ViewSetupData.SomeList}, FallbackValue=12}"
You can put a constant indexer in the Path:
{Binding Path=[(sys:Int32)0], Source={x:Static ut:ViewSetupData.SomeList}}
But you can't bind a property of a Binding, so there's no way to stuff a parameter in there. However, you can combine multiple bindings in a MultiBinding, so you could use one of those with a multi-value converter:
C#:
public class IListIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// You might want a little more error-checking than this...
return ((IList)values[0])[(int)values[1]];
}
public virtual object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return null;
}
}
XAML:
<TextBlock>
<TextBlock.Resources>
<local:IListIndexerConverter x:Key="ListIndexer" />
</TextBlock.Resources>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ListIndexer}">
<Binding Source="{x:Static ut:ViewSetupData.SomeList}" />
<Binding
ElementName="MyComboBox"
Path="SelectedIndex"
/>
</MultiBinding>
</TextBlock.Test>
</TextBlock>
Update
While you were marking this as the solution, I was writing a more complete solution that addressed your need to grab a property from the list item:
C#:
public class ListItemPropertyGetter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var list = values[0] as IList;
var index = (int)(values[1] ?? 0);
var propname = values[2] as String;
object item = list[index];
var prop = item.GetType().GetProperty(propname);
var propvalue = prop.GetValue(item);
return propvalue;
}
catch
{
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
// Gotta put these somewhere
public static List<FontSizeThing> FontSizeThings { get; } =
new List<FontSizeThing>
{
new FontSizeThing(10),
new FontSizeThing(10.5),
new FontSizeThing(11),
new FontSizeThing(12),
new FontSizeThing(14),
new FontSizeThing(15),
};
}
public class FontSizeThing {
public FontSizeThing(double n) { FontSize = n; }
public double FontSize { get; set; }
}
XAML:
<ComboBox x:Name="FontSizeOptionCombo">
<sys:Int32>0</sys:Int32>
<sys:Int32>1</sys:Int32>
<sys:Int32>2</sys:Int32>
<sys:Int32>3</sys:Int32>
<sys:Int32>4</sys:Int32>
</ComboBox>
<TextBlock Text="Testing">
<TextBlock.Resources>
<hconv:ListItemPropertyGetter x:Key="ListItemPropertyGetter" />
</TextBlock.Resources>
<TextBlock.FontSize>
<MultiBinding Converter="{StaticResource ListItemPropertyGetter}" StringFormat="{}{0}">
<Binding Source="{x:Static hconv:ListItemPropertyGetter.FontSizeThings}" />
<Binding ElementName="FontSizeOptionCombo" Path="SelectedItem" />
<Binding Source="FontSize" />
</MultiBinding>
</TextBlock.FontSize>
</TextBlock>
FINAL UPDATE
Note that if I had merely populated FontSizeOptionCombo with the FontThings themselves, I could very simply have bound like this:
<ComboBox
x:Name="OtherCombo"
ItemsSource="{x:Static hconv:ListItemPropertyGetter.FontSizeThings}"
DisplayMemberPath="FontSize"
FontSize="{Binding SelectedItem.FontSize, ElementName=OtherCombo, FallbackValue=20}"
/>
If that fits in with what you're doing, it's by far the nicest way.
In our WPF project, we are using a TextBox to let the user enter some length measures. This measures may be in millimeters, centimeters, inches, etc (depending on the user configuration).
We need to show the numbers on the TextBox along with the current measurement unit, like:
"120 mm"
"16 cm"
"1' 2 3/4"
and so on.
I am using ValueConverter to both perform the unit converting and to add the extra formatting. The UpdateSourceTrigger is default, so the binding applies when the user leaves the control.
The challenge here is that while editing the value, these "mm" or "cm" are not shown. So the previous examples in edit mode would be:
"120"
"16"
"1 2 3/4"
How can I make that happen?
Here is the Converter code for some cenĂ¡rios:
/// <summary>
/// Converts and formats the value (in milimeters) to the required unit/format
/// </summary>
public class LengthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
LengthUnit unit = MainWindow.CurrentLengthUnit;
switch (unit.Id)
{
case 1: //Milimeters
return value + " mm";
case 2: //Centimeters
return (double)value / 10.0 + " cm";
default:
throw new InvalidOperationException("Unknown length unit.");
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
string rawValue = value as string;
LengthUnit unit = MainWindow.CurrentLengthUnit;
switch (unit.Id)
{
case 1: //Milimeters
return rawValue.Replace("mm", "").Trim();
case 2: //Centimeters
return Double.Parse(rawValue.Replace("cm", "").Trim()) * 10.0;
default:
throw new InvalidOperationException("Unknown length unit.");
}
}
}
What about this:
view:
<TextBox>
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="IsFocused" Value="False">
<Setter Property="Text">
<Setter.Value>
<MultiBinding Converter="{StaticResource LengthConverter}">
<Binding Path="Foo"/>
<Binding Path="Text" RelativeSource="{RelativeSource Mode=Self}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Text" Value="{Binding Foo}"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
converter:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Count() > 1)
{
var value = values[1].ToString();
if (value != String.Empty)
{
int output;
Int32.TryParse(value, out output);
if (output != (int)values[0])
return value;
}
}
return String.Format("{0} {1}", values[0], "mm");
}
So far I managed to fix this problem using only MultiBinding and passing the TextBox control as a parameter (not very nice I suppose).
On the converter code, I check for the control property "IsFocused" and return values the way I need. I am not able to pass the "IsFocused" property directly on the binding because it is readonly while the TextBox is two way bound to the source.
The following code shows some additional handlers used to "force" binding when the controls gain or loose focus.
<TextBox x:Name="leadInTextBox"
LostFocus="SizeLostFocus"
MouseDoubleClick="SelectText"
GotKeyboardFocus="SelectText"
PreviewMouseLeftButtonDown="SelectivelyMouseButton"
PreviewTextInput="PreviewTextInput">
<TextBox.Text>
<MultiBinding Mode="TwoWay" Converter="{StaticResource MultiLengthConverter}" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
<Binding Source="{x:Static local:ApplicationService.UserInfo}" Path="LeadIn" StringFormat="F2" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
<Binding RelativeSource="{RelativeSource Self}" Path="." />
</MultiBinding>
</TextBox.Text>
</TextBox>
converter:
public class MultiLengthConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
LengthUnit currentUnit = ApplicationService.UserInfo.LengthUnit;
TextBox txtBox = values[1] as TextBox;
switch (currentUnit)
{
case LengthUnit.Millimeter:
return String.Format("{0:N2}{1}", values[0], txtBox.IsFocused == true ? string.Empty : " mm");
case LengthUnit.Centimeter:
return String.Format("{0:N2}{1}", (double)values[0] / 10.0, txtBox.IsFocused == true ? string.Empty : " cm");
case LengthUnit.DecimalInch:
return String.Format("{0:N2}{1}", (double)values[0] / 25.4, txtBox.IsFocused == true ? string.Empty : " in");
default:
throw new InvalidOperationException("Unknown length unit.");
}
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
string rawValue = value as string;
LengthUnit currentUnit = ApplicationService.UserInfo.LengthUnit;
object ret;
switch (currentUnit)
{
case LengthUnit.Millimeter:
ret = Double.Parse(rawValue.Replace("mm", "").Trim());
break;
case LengthUnit.Centimeter:
ret = Double.Parse(rawValue.Replace("cm", "").Trim()) * 10.0;
break;
case LengthUnit.DecimalInch:
ret = Double.Parse(rawValue.Replace("in", "").Trim()) * 25.4;
break;
default:
throw new InvalidOperationException("Unknown length unit.");
}
return new object[] { ret, null };
}
}
I have a textbox that is bound to a class with a property of type Timespan, and have written a value converter to convert a string into TimeSpan.
If a non number is entered into the textbox, I would like a custom error message to be displayed (rather than the default 'input string is in the wrong format').
The converter code is:
public object ConvertBack(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
try
{
int minutes = System.Convert.ToInt32(value);
return new TimeSpan(0, minutes, 0);
}
catch
{
throw new FormatException("Please enter a number");
}
}
I have set 'ValidatesOnExceptions=True' in the XAML binding.
However, I have come across the following MSDN article, which explains why the above will not work:
"The data binding engine does not catch exceptions that are thrown by a user-supplied converter. Any exception that is thrown by the Convert method, or any uncaught exceptions that are thrown by methods that the Convert method calls, are treated as run-time errors"
I have read that 'ValidatesOnExceptions does catch exceptions in TypeConverters, so my specific questions are:
When would you use a TypeConverter over a ValueConverter
Assuming a TypeConverter isn't the answer to the issue above, how can I display my custom error message in the UI
I would use a ValidationRule for that, this way the converter can be sure that the conversion works since it only is called if validation succeeds and you can make use of the attached property Validation.Errors which will contain the errors your ValidationRule creates if the input is not the way you want it.
e.g. (note the tooltip binding)
<TextBox>
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
<TextBox.Text>
<Binding Path="Uri">
<Binding.ValidationRules>
<vr:UriValidationRule />
</Binding.ValidationRules>
<Binding.Converter>
<vc:UriToStringConverter />
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
I used validation and converter to accept null and numbers
XAML:
<TextBox x:Name="HeightTextBox" Validation.Error="Validation_Error">
<TextBox.Text>
<Binding Path="Height"
UpdateSourceTrigger="PropertyChanged"
ValidatesOnDataErrors="True"
NotifyOnValidationError="True"
Converter="{StaticResource NullableValueConverter}">
<Binding.ValidationRules>
<v:NumericFieldValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Code Behind:
private void Validation_Error(object sender, ValidationErrorEventArgs e)
{
if (e.Action == ValidationErrorEventAction.Added)
_noOfErrorsOnScreen++;
else
_noOfErrorsOnScreen--;
}
private void Confirm_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = _noOfErrorsOnScreen == 0;
e.Handled = true;
}
ValidationRule :
public class NumericFieldValidation : ValidationRule
{
private const string InvalidInput = "Please enter valid number!";
// Implementing the abstract method in the Validation Rule class
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
float val;
if (!string.IsNullOrEmpty((string)value))
{
// Validates weather Non numeric values are entered as the Age
if (!float.TryParse(value.ToString(), out val))
{
return new ValidationResult(false, InvalidInput);
}
}
return new ValidationResult(true, null);
}
}
Converter :
public class NullableValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (string.IsNullOrEmpty(value.ToString()))
return null;
return value;
}
}
You shouldn't throw exceptions from the converter. I would implement IDataErrorInfo and implement the Error and String on that. Please check https://web.archive.org/web/20110528131712/http://www.codegod.biz/WebAppCodeGod/WPF-IDataErrorInfo-and-Databinding-AID416.aspx.
HTH daniell
I have the current DataTrigger:
<DataTrigger Binding="{Binding HeaderType}" Value="1">
<Setter Property="BorderThickness" Value="5"/></DataTrigger>
I want to do the same with values 2-100
Do I have to copy the Data Trigger 99 times or maybe there's a better way ?
Add a property to your view model:
public bool HasImportantHeader // or something...
{
get { return HeaderType >=1 && HeaderType <= 100; }
}
Use that property in the data trigger:
<DataTrigger Binding="{Binding HasImportantHeader}" Value="True">
<Setter Property="BorderThickness" Value="5"/>
</DataTrigger>
Generally, I like to keep my XAML as simple as possible, put all the logic in the view model, and avoid using Converters unless they are absolutely necessary.
Let's say you add another view, where you want to use bold text to indicate the header type is between 1 and 100. Just re-use the HasImportantHeader property, something like:
<DataTrigger Binding="{Binding HasImportantHeader}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
Later, you may decide that all header types up to 200 should have thick border and bold text. It'll be a simple matter of changing the implementation of the HasImportantHeader property.
I've used this in similar situations
<DataTrigger Binding="{Binding HeaderType,
Converter={StaticResource RangeConverter},
ConverterParameter=1-100}"
Value="True">
<Setter Property="BorderThickness" Value="5"/>
</DataTrigger>
And in the converter we return true or false depending on the ranges
public class RangeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string[] ranges = parameter.ToString().Split(new char[]{'-'});
int headerType = (int)value;
if (headerType >= System.Convert.ToInt32(ranges[0]) &&
headerType <= System.Convert.ToInt32(ranges[1]))
{
return true;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
You need to use a converter for that purpose.You can add a converter on your DataTrigger.
The Converter will allow you to pass in the value, and return true or false.
<DataTrigger
Binding="{Binding HeaderType, Converter={StaticResource RengeConvertor}}"
Value="true"
>
<Setter Property="BorderThickness" Value="5" />
</DataTrigger>
and your converter should look something like
public class RengeConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int data = (int)value;
if (data >= 2 && data <= 100)
return true;
else
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You may also find this interesting http://zamjad.wordpress.com/2010/07/29/range-converter/
I have 4 buttons in grid which datacontext is set to an object which has property that indicates what button should be enabled (it's enumerable).
Currenty I have done this in code-behind so that when that specific property changes, it disables all but one depending on the value. It works, but I really don't like to put stuff like this to code-behind. There must be a way to do this in xaml?
I could make own style for all four buttons and then do this with data triggers, but I would prefer more generic approach: use same style for all buttons that somehow applies differently depending on, for example, a button name and value of the property.
Thanks in advance.
You could use a MultiBinding to bind the IsEnabled property to a combination of the control's name and the property from your DataContext, and create a Style to apply it to all buttons in the Grid:
<Grid.Resources>
<local:EqualsConverter x:Key="EqualsConverter"/>
<Style TargetType="Button">
<Setter Property="IsEnabled">
<Setter.Value>
<MultiBinding Converter="{StaticResource EqualsConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Name"/>
<Binding Path="EnabledButtonName"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
And in code:
public class EqualsConverter
: IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return values.Length == 2 && object.Equals(values[0], values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
You can create an association between the enum you've created and the buttons by having integer references to your enum values and giving those enum values as ConverterParameters for corresponding buttons.
For Eg:
The enum:
public enum myOptions
{
value1 = 1,
value2 = 2,
value3 = 3,
value4 = 4
}
The Binding:
<Button IsEnabled = {Binding Path=myProperty,
Converter = {StaticResource EnumToBoolConverter},
ConverterParameter = 1} />
<Button IsEnabled = {Binding Path=myProperty,
Converter = {StaticResource EnumToBoolConverter},
ConverterParameter = 2} />
And the Converter:
public class EnumToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value == (int)parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}