I am trying to bind to a dependency property of my user control from my user control however it doesn't seem to be working as the converter keeps throwing an unset dependency property error
The dependency property
public DateTime? DisplayedDate
{
get { return (DateTime?)base.GetValue(DisplayedDateProperty); }
set { base.SetValue(DisplayedDateProperty, value); }
}
public static readonly DependencyProperty DisplayedDateProperty =
DependencyProperty.Register("DisplayedDate", typeof(DateTime?), typeof(SideBarUser), new FrameworkPropertyMetadata()
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
});
The XAML Binding
<UserControl.Resources>
<sys:Int32 x:Key="Test">1</sys:Int32>
<Converters:DateCountConverter x:Key="DateCountConverter"/>
</UserControl.Resources>
<TextBlock DataContext="{Binding RelativeSource={RelativeSource Self}}"
TextAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DateCountConverter}">
<Binding Path="DisplayedDate" />
<Binding Source="{StaticResource Test}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
And finally the part it's failing at in the coverter
DateTime date = (DateTime)values[0];
All together yields
System.InvalidCastException
Specified cast is not valid.
at System.Windows.Data.MultiBindingExpression.TransferValue()
at System.Windows.Data.MultiBindingExpression.Transfer()
at System.Windows.Data.MultiBindingExpression.UpdateTarget(Boolean includeInnerBindings)
at System.Windows.Data.MultiBindingExpression.AttachToContext(Boolean lastChance)
at System.Windows.Data.MultiBindingExpression.MS.Internal.Data.IDataBindEngineClient.AttachToContext(Boolean lastChance)
at MS.Internal.Data.DataBindEngine.Task.Run(Boolean lastChance)
at MS.Internal.Data.DataBindEngine.Run(Object arg)
at MS.Internal.Data.DataBindEngine.OnLayoutUpdated(Object sender, EventArgs e)
at System.Windows.ContextLayoutManager.fireLayoutUpdateEvent()
at System.Windows.ContextLayoutManager.UpdateLayout()
at System.Windows.UIElement.UpdateLayout()
at System.Windows.Interop.HwndSource.SetLayoutSize()
at System.Windows.Interop.HwndSource.set_RootVisualInternal(Visual value)
at System.Windows.Interop.HwndSource.set_RootVisual(Visual value)
at MS.Internal.DeferredHwndSource.ProcessQueue(Object sender, EventArgs e)
I cannot seem to get this to work for the life of me. Am I missing something? When debugging with another instance of Visual Studio it comes up that it is an Unset Dependency property
Edit:
When I comment out everything and just have
<TextBlock Text="{Binding Path=DisplayedDate, RelativeSource={RelativeSource Self}}" />
It works just fine displaying the display date. My confusion level is too great to cope with right now
EDIT EDIT:
Converter code
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime? date = (DateTime?)values[0];
//ToDo move most of the logic inside AppointmentsViewModel class to handle date filtering
AppointmentsViewModel MyAppointments = new AppointmentsViewModel();
String Count;
int SelectionType = (int)values[1];
//Note To Self Make Enum
switch (SelectionType)
{
case 0:
Count = MyAppointments.Appointments.Where(x => date != null && x.Beginning.HasValue && date.HasValue
&& x.Beginning.Value.Month == date.Value.Month
&& x.Beginning.Value.Year == date.Value.Year ).Count().ToString();
break;
case 1:
Count = MyAppointments.Appointments.Where(x => date != null && x.Test.HasValue && date.HasValue
&& x.Test.Value.Month == date.Value.Month
&& x.Test.Value.Year == date.Value.Year).Count().ToString();
break;
//ETC
default:
Count = MyAppointments.Appointments.Where(x => date != null && x.End.HasValue
&& date.HasValue && x.End.Value.Month == date.Value.Month
&& x.End.Value.Year == date.Value.Year).Count().ToString();
break;
}
return Count;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
There are several problems with your code. I had to make some assumptions here, so hopefully I'm correct.
The converter
Your converter assumes that it will get 2 values of certain types. You want to be a little careful with that. Especially the first value, which comes from a binding, might be DependencyProperty.UnsetValue if the binding has not been set yet.
So, you probably want to check if the values are correct before you start doing the actual conversion, for example:
if (values.Length != 2 || !(values[0] is DateTime?)|| !(values[1] is int))
{
return DependencyProperty.UnsetValue;
}
You should not have your converter throw exceptions, because they are treated as uncaught run-time exceptions and will terminate your application unless you have some global exception handler (see this question).
The control
Now, I assume that your DisplayedDate property is defined on your UserControl. If so, then this line:
<TextBlock DataContext="{Binding RelativeSource={RelativeSource Self}}"
will set the DataContext to this TextBlock, so when you later go to retrieve the DisplayedDate property, it will not be found. You can fix this in 2 ways:
1) You use the ancestor-lookup binding:
"{Binding RelativeSource={RelativeSource AncestorType=local:UserControl1}}"
Of course, replace local:UserControl1 with the namespace and name of your control.
2) You define the content of the UserControl as a Template instead, then use {RelativeSource TemplatedParent}, which will point to the "owner" of the template, in this case, your UserControl:
<UserControl.Template>
<ControlTemplate>
<TextBlock DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}"
TextAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DateCountConverter}">
<Binding Path="DisplayedDate" />
<Binding Source="{StaticResource Test}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</ControlTemplate>
</UserControl.Template>
Just put this in your XAML instead of the <TextBlock>...</TextBlock> part.
Maybe there are some other issues, but testing with a simple converter this worked for me.
Related
I am trying to create a re-usable user control (for data entry) in which there are two text boxes and they are linked to each by an IValueConvertor.
The following XAML is the original, normal code. This is what I am trying to reproduce in a user control.
<WrapPanel>
<TextBlock Text="Length of Fence"/>
<TextBox Name="Metric" Width="50" Text="{Binding Path=LengthFence, Mode=TwoWay}"/>
<TextBlock Text="Meters"/>
<TextBox Text="{Binding ElementName=Metric, Path=Text, Converter={StaticResource MetersToInches}, StringFormat=N8}"/>
<TextBlock Text="Inches"/>
</WrapPanel>
and the code-behind for the IValueConvertor (in MainWindow.xaml) is
public class MetersToInches : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.ToString() == "")
return 0.0;
try
{
double meters = System.Convert.ToDouble(value);
var result = meters * 39.3701;
return result;
}
catch
{
// Catch errors when users type invalid expressions.
return 0.0;
}
}
public object ConvertBack(object value, Type targettype, object parameter, CultureInfo culture)
{
if (value.ToString() == "")
return 0.0;
try
{
double inches = System.Convert.ToDouble(value);
var result = inches * 0.0254;
return result;
}
catch
{
// Catch errors when users type invalid expressions.
return 0.0;
}
}
}
This is what this XAML looks like:
Now I have made a re-usable UserControl with three dependency properties Label for label string, Value for binding a property inside the ViewModel, and Units - a string property to show the input units.
<UserControl ...
x:Name="parent">
<StackPanel DataContext="{Binding ElementName=parent}">
<TextBlock Text="{Binding Path=Label}"/>
<TextBox Text="{Binding Path=Value}"/>
<TextBlock Text="{Binding Path=Units}"/>
</StackPanel>
However, this re-usable control can only tackle the first TextBox of the input. I do not know how to bind the IValueConvertor in the second TextBox. I need to do this because I want to bind other converters such as meters to feet, kg to pound, etc.
I have read that ConvertorParameter cannot be bound because it is not a dependency property and I am not sure if I can use multi-binding, mostly because I do not know how to use it properly Binding ConverterParameter.
I would be very grateful if you could show me how to do this or direct me to the appropriate link on StackOverflow or elsewhere that solves this problem. Or if there is a better way of doing this.
Many many thanks in advance.
First, don't bind the TextBoxes to each other (as in your original code at the begining of the question), instead, bind each TextBox to the same backing property, which, in your UserControl, is Value.
As for how to implement multiple bindings, you probably don't need a MultiBinding.
We have to pick a "standard" unit of measure to begin with- this will be the unit that will be actually stored in the property and in any database or file. I'll assume this standard unit will be meters (m). An IValueConverter can be used to convert between meters and some other unit of distance and back, using the ConverterParameter to specify which other unit to convert to/from.
Here's a good example to get you started.
public enum DistanceUnit { Meter, Foot, Inch, }
public class DistanceUnitConverter : IValueConverter
{
private static Dictionary<DistanceUnit, double> conversions = new Dictionary<DistanceUnit, double>
{
{ DistanceUnit.Meter, 1 },
{ DistanceUnit.Foot, 3.28084 },
{ DistanceUnit.Inch, 39.37008 }
};
//Converts a meter into another unit
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return conversions[(DistanceUnit)parameter] * (double)value;
}
//Converts some unit into a meter
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) { return 0; }
double v;
var s = value as string;
if (s == null)
{
v = (double)value;
}
else
{
if (s == string.Empty) { return 0; }
v = double.Parse(s);
}
if (v == 0) { return 0; }
return v / conversions[((DistanceUnit)parameter)];
}
}
The above has a few problems. I never check if parameter really is a DistanceUnit before using it, for example. But it works.
Here's an example of how I used it:
<StackPanel>
<StackPanel.Resources>
<local:DistanceUnitConverter x:Key="DistCon"/>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Meter}}" MinWidth="20"/>
<TextBlock>m</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Foot}}" MinWidth="20"/>
<TextBlock>ft</TextBlock>
</StackPanel>
</StackPanel>
The DistanceUnit enum and the internal conversions dictionary can be expanded with more units of measure. Alternatively, you can use a 3rd party library that already has all these included, like UnitsNet.
Not sure how you would like to bind mulitple converters in one single control. If i'm not wrong, you would like to build a control where when a user enters a particular value, you need to display it in different units. If this is the case, you can create a single converter with converterparameter as "m","cm","inch" etc and based on this you can return the result. Then in this case, you will have 4,5 controls and each will have same converter binding but different converter values. If this is not clear and you need further direction, please let know.
Multi Value binding
To answer your point 6, please see a sample multi binding converter and its implementation in xaml below. I have built a simple RolesFilter which will take different inputs from the xaml as object[] and since I already know what data is expected, i'm converting them in the converter.
public class RolesFilter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
FlipperObservableCollection<Role> _roles = (FlipperObservableCollection<Role>)values[0]; //Input
Department _dept_param = values[1] as Department;
bool _filter = (bool)values[2];
string _id = "NA";
if (values.Count() == 4 && values[3] is string) _id = (string)values[3] ?? "NA";
//If we need a filter, then without department, it should return empty results
if (!_filter) return _roles; //If no filter is required, then don't worry, go ahead with input values.
if (_dept_param == null) return new FlipperObservableCollection<Role>(); //If department is null, then
List<Role> _filtered_list = _roles.ToList().Where(p => p.department.id == _dept_param.id && p.id != _id)?.ToList() ?? new List<Role>();
return new FlipperObservableCollection<Role>(_filtered_list);
}
catch (Exception)
{
throw;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I'm using the multi value converter in the xaml as below. Here, i'm filtering an itemsource of a combo box based on another combobox and a check box. This is just an example and in your case, you can create a combo box with different Units values. Based on user selection, you can use the converter and return value to the textbox.
<ComboBox Height="30" SelectedItem="{Binding reports_to, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource roles_filter}">
<Binding Source="{StaticResource SingletonData__}" Path="roles" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="department" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
<Binding ElementName="cbx_filter" Path="IsChecked"/>
<Binding Path="id" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding department.name}"/>
<TextBlock Text=" - "/>
<TextBlock Text="{Binding name}"/>
</WrapPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I am loading items from database to my DataGrid on this way (on form load):
dataGridArticles.ItemsSource = null;
dataGridArticles.ItemsSource = listCurrentArticles;
And I got ability to add item to my datagrid list, and also I have ability to remove them by pressing DEL key <datagrid automatically removes it from a list>.
But When I am entering item to my list which is source to datagrid I am also setting OrdinalNumber for each item, for example when I adding new item to a list I am doing next thing:
article.OrdinalNumber = GetCurrentlyMaxOrdinalNumber()+1;
so if I'm adding items one after other, ordinals numbers will be 1,2,3,4,5... and so on, something like this (marked red) :
My stored procedure which is returning max ordinal number from articles table:
CREATE DEFINER=`root`#`localhost` PROCEDURE `ArticleGetNextCodeNumber`()
BEGIN
Select ifnull(max(OriginalArticleCode),0)
from articles;
END
And this is what is happening when I am adding new item to my list:
I am setting next ordinal numbers and some other attributes for new article:
private void btnSaveChangesOnArticle_Click(object sender, RoutedEventArgs e)
{
try
{
if (newItem)
{
Article newArticle = new Article();
newArticle.ArticleCode = txtArticleCode.Text.Trim();
newArticle.OriginalArticleCode = Convert.ToInt32(ArticlesController.Instance.GetNextArticleCode()) + 1; // Here I'm setting new article ordinal number
newArticle.Title = txtArticleTitle.Text.Trim();
newArticle.Price = Convert.ToDecimal(txtPrice.Text.Trim());
var lastInserted = ArticlesController.Instance.Save(newArticle);
MessageBox.Show("Operation Successful.");
listCurrentArticles.Add(lastInserted);
dataGridArticles.ItemsSource = null;
dataGridArticles.ItemsSource = listCurrentArticles;
}
}
}
}
And I am wondering right now, when user press DELETE KEY to remove some item from my datagrid,how can I reorganize my ordinal numbers, because if I add 5 items for example to my datagrid, and if user delete second item, than my order might look like this: 1 3 4 5.. insted of 1 2 3 4
So my question is guys, how can i reorganize that, when something changes to keep it sequenced all the time ( so maybe then I dont need to keep ordinal number in database, because it is not important to me, I am using it just because of better UI look, to show to user how many items he added to that DataGrid).
Thanks a lot,
Cheers!
You could replace the OrdinalNumber DataGridTextColumn with a DataGridTemplateColumn and use a converter to return the value of the GetIndex() method of the parent DataGridRow container.
You need to bind to both the parent DataGridRow and the Count property of the Items property of the DataGrid itself for the binding to get refreshed when the number of items changes, i.e. when an item is added or removed.
This should be pretty simple to implement. You create a class that implements the IMultiValueConverter interface:
namespace WpfApplication3
{
public class RowNumberConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return ((values[0] as DataGridRow).GetIndex() + 1).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
And use it like this:
<DataGrid x:Name="dataGridArticles" AutoGenerateColumns="False" xmlns:local="clr-namespace:WpfApplication3">
<DataGrid.Resources>
<local:RowNumberConverter x:Key="RowNumberConverter" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="#" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource RowNumberConverter}">
<Binding Path="." RelativeSource="{RelativeSource AncestorType=DataGridRow}" />
<Binding Path="Items.Count" RelativeSource="{RelativeSource AncestorType=DataGrid}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- + the rest of your columns -->
</DataGrid.Columns>
</DataGrid>
Check out this related question that might solve your problem.
Column/Row index in a DataGrid column
Your best option is to remove OrdinalNumber property (Since you do not need it, as you said) and use DataGrid.RowHeaderTemplate with a converter. Here is a working example:
MainWindow
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
listCurrentArticles = new ObservableCollection<Article>() { new Article { Text = "k" }, new Article { Text = "l" } };
}
public ObservableCollection<Article> listCurrentArticles { get; set; }
Xaml
<Window.Resources>
<local:HeaderConverter x:Key="headerConverter"/>
</Window.Resources>
<Grid>
<DataGrid DataContext="{Binding}" ItemsSource="{Binding listCurrentArticles}">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<TextBlock MinWidth="25" TextAlignment="Center">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource headerConverter}">
<Binding Path="ItemsSource" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=DataGrid}" />
<Binding Path="Item" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=DataGridRow}"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
</Grid>
Converter
public class HeaderConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<Article> list = values[0] as ObservableCollection<Article>;
Article obj = values[1] as Article;
int ind = list.IndexOf(obj);
if (ind == -1)
return "+";
else
return (ind + 1).ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I have the following scenario: In my View Model I have two properties, let them be CreateNewThing and SelectedExistingThing. CreateNewThing is of type bool and SelectedExistingThing is of type Thing.
I now have a ComboBox that displays two static entries, "None" and "Create new", and a list of Things. I wired it up like this:
Collection for the list of things:
<ComboBox.Resources>
<CollectionViewSource x:Key="AllThings" Source="{Binding ViewModel.AllThings, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}}" />
<tools:ThingSelector x:Key="ThingSelector" />
</ComboBox.Resources>
Setup for the ItemsSource:
<ComboBox.ItemsSource>
<CompositeCollection>
<tools:PlaceholderForNone />
<tools:PlaceholderForNew />
<Separator />
<CollectionContainer Collection="{Binding Mode=OneWay, Source={StaticResource AllThings}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
Binding for SelectedValue:
<ComboBox.SelectedValue>
<MultiBinding Converter="{StaticResource ThingSelector}">
<Binding Path="ViewModel.CreateNewThing" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}" />
<Binding Path="ViewModel.SelectedExistingThing" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyCustomControl}}" />
</MultiBinding>
</ComboBox.SelectedValue>
My ThingSelector looks like this:
internal sealed class ThingSelector: IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
if((values == null) ||
(values.Length != 2) ||
(!(values[0] is bool))) {
return Binding.DoNothing;
}
if((bool)values[0]) {
return new PlaceholderForNew();
}
if(values[1] is Thing) {
return values[1];
}
return new PlaceholderForNone();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
if(value is PlaceholderForNew) {
return new object[] { true, null };
}
return new object[] { false, value as Thing};
}
}
The PlaceholderForXXX are done in a way that their HashCode is always 0 and Equals is true for all objects of the same type, so all two PlaceholderForXXX are equal.
The strange thing is now: I can select the "None" and "Create new" options from my combo box, and they are correctly propagated (i.e., selecting the "new" option sets the CreateNewThing property to true and the SelectedExistingThing property to null).
However, when I select a Thing from the combo box, the ConvertBack method is invoked with the parameter value being null where, to my understanding, it should be the selected value of the ComboBox which is clearly not null (if I put an appropriate event into the code-behind, I really see that SelectedValue is a Thing and not null).
What am I missing?
I have a sample where I bind a view model's properties with some TextBox controls, including validation rules. In most cases, this works fine. But when I try to include the IsFocused property of the bound TextBox, I am having trouble in the case when an invalid number is entered in the control.
When I input the wrong number in the TextBox controls that are bound directly to the view model's property, the errors are shown as expected (red border around the TextBox). But in the TextBox that is bound with a MultiBinding that includes both the view model property and the IsFocused property of the TextBox, the error is not shown and the value gets reset to the previous valid value.
For example, if a number less than 10 is invalid, and I input 3, when the TextBox loses focus, a red border normally would appear in the TextBox signaling the error. But in the TextBox which includes IsFocused as a source for its binding, the value changes back to the previous valid value (if there was a 39 before I entered 3, the TextBox changes back to 39).
Using the code below you can reproduce the issue:
TestViewModel.cs
public class TestViewModel
{
public double? NullableValue { get; set; }
}
MainWindow.xaml
<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:TestViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Nullable: "/>
<TextBox VerticalAlignment="Top" Grid.Column="1">
<TextBox.Text>
<MultiBinding Mode="TwoWay">
<Binding Path="NullableValue"/>
<Binding Path="IsFocused"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<MultiBinding.ValidationRules>
<l:ValidateIsBiggerThanTen/>
</MultiBinding.ValidationRules>
<MultiBinding.Converter>
<l:TestMultiBindingConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBox VerticalAlignment="Top" Grid.Column="2"/>
</Grid>
</Window>
TestMultiBindingConverter.cs
public class TestMultiBindingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] != null)
return values[0].ToString();
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (value != null)
{
double doubleValue;
var stringValue = value.ToString();
if (Double.TryParse(stringValue, out doubleValue))
{
object[] values = { doubleValue };
return values;
}
}
object[] values2 = { DependencyProperty.UnsetValue };
return values2;
}
}
ValidateIsBiggerThanTen.cs
public class ValidateIsBiggerThanTen : ValidationRule
{
private const string errorMessage = "The number must be bigger than 10";
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var error = new ValidationResult(false, errorMessage);
if (value == null)
return new ValidationResult(true, null);
var stringValue = value.ToString();
double doubleValue;
if (!Double.TryParse(stringValue, out doubleValue))
return new ValidationResult(true, null);
if (doubleValue <= 10)
return error;
return new ValidationResult(true, null);
}
}
Why are the errors not showing for the TextBox in the above example?
The cause of the behavior you're seeing is specifically that you've bound the TextBox's IsFocused property in your MultiBinding. This directly has the effect of forcing an update of the target of the binding when the focus changes.
In the scenario where validation fails, there is a very brief moment when the validation rule has fired, the error is set, but the focus hasn't actually been changed yet. But this all happens too fast for a user to see. And since validation failed, the source of the binding is not updated.
So when the IsFocused property value changes, after the validation and rejection of the entered value happens, the next thing to happen is that the binding is re-evaluated (because one of the source properties changed!) to update the target. And since the actual source value never changed, the target (the TextBox) reverts from whatever you typed back to whatever was stored in the source.
How should you fix this? It depends on the exact behavior desired. You have three basic options:
Keep binding to IsFocused, and add UpdateSourceTrigger="PropertyChanged". This will keep the basic current behavior of copying the old value back when focus is lost, but will at least provide the user with immediate validation feedback as the value is edited.
Remove binding to IsFocused altogether. Then the target of the binding won't depend on that, and won't be re-evaluated when focus changes. Problem solved. :)
Keep binding to IsFocused, and add logic so that the interaction with validation does not result in copying a stale value back to the TextBox.
Based on our comments back and forth, it seems that the third option above is the preferred one for your scenario, as you desire to format the text representation of the value differently when the control has focus vs. when it does not.
I am skeptical of the wisdom of a user interface that formats data differently depending on whether the control is focused or not. Of course, it makes complete sense for focus changes to affect the overall visual presentation, but that would generally involve things like underlining, highlighting, etc. Displaying a completely different string depending on whether the control is focused seems likely to interfere with user comprehension and possibly annoy them as well.
But I'm in agreement that this is a subjective point, and clearly in your case you have this specific behavior that is desirable for your specification and needs to be supported. So with that in mind, let's look at how you can accomplish that behavior…
If you want to be able to bind to the IsFocused property, but not have changes to focus copy over the current contents of the control if the source has not actually been updated yet (i.e. if a validation error prevented that from happening), then you can also bind to the Validation.HasError property, and use that to control the converter's behavior. For example:
class TestMultiBindingConverter : IMultiValueConverter
{
private bool _hadError;
public object Convert(object[] values,
Type targetType, object parameter, CultureInfo culture)
{
bool? isFocused = values[1] as bool?,
hasError = values[2] as bool?;
if ((hasError == true) || _hadError)
{
_hadError = true;
return Binding.DoNothing;
}
if (values[0] != null)
{
return values[0].ToString() + (isFocused == true ? "" : " (+)");
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value,
Type[] targetTypes, object parameter, CultureInfo culture)
{
if (value != null)
{
double doubleValue;
var stringValue = value.ToString();
if (Double.TryParse(stringValue, out doubleValue))
{
object[] values = { doubleValue };
_hadError = false;
return values;
}
}
object[] values2 = { DependencyProperty.UnsetValue };
return values2;
}
}
The above adds a field _hadError that "remembers" what's happened recently to the control. If the converter is called while validation is detecting an error, the converter returns Binding.DoNothing (which has the effect its name suggests :) ), and sets the flag. Thereafter, no matter what happens, as long as that flag is set the converter will always do nothing.
The only way that the flag will get cleared is if the user eventually enters text that is valid. Then the converter's ConvertBack() method will be called to update the source, and in doing so it can clear the _hadError flag. This ensures that the control contents will never get overwritten due to binding updates, except when there has been no error since the last time the source was updated.
Here's the XAML example above updated to use the additional binding input:
<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:TestViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Nulleable: "/>
<TextBox x:Name="textBoxWrapper" Grid.Column="1" VerticalAlignment="Top">
<TextBox.Text>
<MultiBinding x:Name="TextBoxBinding" Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged">
<Binding Path="NulleableValue"/>
<Binding Path="IsFocused"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<Binding Path="(Validation.HasError)"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<MultiBinding.ValidationRules>
<l:ValidateIsBiggerThanTen/>
</MultiBinding.ValidationRules>
<MultiBinding.Converter>
<l:TestMultiBindingConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBox VerticalAlignment="Top" Grid.Column="2"/>
</Grid>
</Window>
I should point out, in case it's not obvious: the _hadError field is for the converter itself. For the above to work correctly, you'll need a separate instance of the converter for each binding to which it's applied. There are alternative ways to track such a flag for each control uniquely, but I feel an extended discussion of the options in that respect are outside the scope of this question. Feel free to explore on your own, and post a new question regarding that aspect if you are unable to address the issue adequately on your own.
I am doing localization in .NET 4 with a ResourceDictionary. Does anyone have a solution for using a value with string format?
For instance, let's say I have a value with the key "SomeKey":
<ResourceDictionary ...>
<s:String x:Key="SomeKey">You ran {0} miles</s:String>
</ResourceDictionary>
Using it in a TextBlock:
<TextBlock Text="{DynamicResource SomeKey}" />
How would I combine, for example, an integer with the value of SomeKey as a format string?
You need to bind to a ViewModel.Value somehow, and then use a (nested) binding to a format string.
When you have only one value:
<TextBlock
Text="{Binding Path=DemoValue, StringFormat={StaticResource SomeKey}}" />
When you also have {1} etc then you need MultiBinding.
Edit:
When you really want to change languages in a live Form then the sensible way is probably to do all formatting in the ViewModel. I rarely use StringFormat or MultiBinding in MVVM anyway.
So, I finally came up with a solution that allows me to have format strings in my ResourceDictionary and be able to dynamically change the language at runtime. I think it could be improved, but it works.
This class converts the resource key into its value from the ResourceDictionary:
public class Localization
{
public static object GetResource(DependencyObject obj)
{
return (object)obj.GetValue(ResourceProperty);
}
public static void SetResource(DependencyObject obj, object value)
{
obj.SetValue(ResourceProperty, value);
}
// Using a DependencyProperty as the backing store for Resource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ResourceProperty =
DependencyProperty.RegisterAttached("Resource", typeof(object), typeof(Localization), new PropertyMetadata(null, OnResourceChanged));
private static void OnResourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//check if ResourceReferenceExpression is already registered
if (d.ReadLocalValue(ResourceProperty).GetType().Name == "ResourceReferenceExpression")
return;
var fe = d as FrameworkElement;
if (fe == null)
return;
//register ResourceReferenceExpression - what DynamicResourceExtension outputs in ProvideValue
fe.SetResourceReference(ResourceProperty, e.NewValue);
}
}
This class allows the value from the ResourceDictionary to be used as the format parameter in String.Format()
public class FormatStringConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[0] == null)
return String.Empty;
var format = (string)values[0];
var args = values.Where((o, i) => { return i != 0; }).ToArray();
return String.Format(format, args);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Example Usage 1: In this example, I use the FormatStringConverter in the MultiBinding to convert its Binding collection into the desired output. If, for instance, the value of "SomeKey" is "The object id is {0}" and the value of "Id" is "1" then the output will become "The object id is 1".
<TextBlock ap:Localization.Resource="SomeKey">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource formatStringConverter}">
<Binding Path="(ap:Localization.Resource)" RelativeSource="{RelativeSource Self}" />
<Binding Path="Id" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Example Usage 2: In this example, I use a binding with a Converter to change the resource key to something more verbose to prevent key collisions. If, for instance, I have the enum value Enum.Value (displayed by default as "Value"), I use the converter to attach its namespace to make a more unique key. So the value becomes "My.Enums.Namespace.Enum.Value". Then the Text property will resolve with whatever the value of "My.Enums.Namespace.Enum.Value" is in the ResourceDictionary.
<ComboBox ItemsSource="{Binding Enums}"
SelectedItem="{Binding SelectedEnum}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock ap:Localization.Resource="{Binding Converter={StaticResource enumToResourceKeyConverter}}"
Text="{Binding Path=ap:Localization.Resource), RelativeSource={RelativeSource Self}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Example Usage 3: In this example, the key is a literal and is used only to find its corresponding value in the ResourceDictionary. If, for instance, "SomeKey" has the value "SomeValue" then it will simply output "SomeValue".
<TextBlock ap:Localization.Resource="SomeKey"
Text="{Binding Path=ap:Localization.Resource), RelativeSource={RelativeSource Self}}"/>
If you're trying to bind and format a Miles property to a 'TextBlock' you can do as follows:
<TextBlock Text="{Binding Miles, StringFormat={StaticResource SomeKey}}"/>