1

Well, this little quirk of WPF is really getting under my skin. So, I have a DataGrid like this:

<DataGrid x:Name="Rates" AlternatingRowBackground="#FCFCFC" AutoGenerateColumns="False" CanUserReorderColumns="True" CanUserResizeColumns="True" CanUserAddRows="False" SelectionUnit="FullRow" CanUserSortColumns="True">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Item" Width="Auto" IsReadOnly="True" Binding="{Binding item}"/>
        <DataGridTextColumn Header="Rate" Width ="*" Binding="{Binding rate, UpdateSourceTrigger=PropertyChanged}"/>
    </DataGrid.Columns>

It simply has a list of items in column A and we have to enter the rates in column B. Sounds simple enough.

Here is the property in the ViewModel to which it is bound.

private BindableCollection<Rate> _rates;

public BindableCollection<Rate> Rates
{
    get
    {
        return _rates;
    }

    set
    {
        if (value == _rates)
        {
            return;
        }

        _rates = value;
        NotifyOfPropertyChange();
    }
}

Here is the definition for Rate:

public partial class Rate
{
    public int ID { get; set; }
    public string client { get; set; }
    public string item { get; set; }
    public Nullable<decimal> rate { get; set; }
}

So, the second column of my DataGrid is getting bound to Rate.rate which is a Nullable<decimal>. Herein lies the problem. The Nullable<decimal> does accept NULL but the TextBox wouldn't let me enter a blank value. Moreover, if I type in some value like 5 and then delete it, the ViewModel retains the previous value.

I have seen solutions involving the IValueConverter, but I would prefer a XAML-based solution. Is there any? I have several modules where the DataType is Nullable, and it can quickly become very tedious if I have to use IValueConverter every time.

EDIT

I am using Caliburn.Micro, hence using TargetNullValue='' will not work everywhere, as the binding is automatic. It'll work in the above example, where the binding is explicitly mentioned.

Il Vic
  • 5,576
  • 4
  • 26
  • 37
  • One hack can be to replace Nullable decimal with string and then to validate user input or even to restrict user input to only allow decimal values by extending textbox class text changed event or previewKeydown event. – Naresh Ravlani Jun 08 '17 at 05:11

2 Answers2

2

If you are using Caliburn.Micro, you can configure the way CM applies automatic bindings between your View and your ViewModel.

So in the Configure method of your Bootstrapper classe you can write something like:

protected override void Configure()
{
    ConventionManager.SetBinding = delegate(Type viewModelType, string path, PropertyInfo property, FrameworkElement element, ElementConvention convention, DependencyProperty bindableProperty)
    {
        Binding binding = new Binding(path);
        ConventionManager.ApplyBindingMode(binding, property);
        ConventionManager.ApplyValueConverter(binding, bindableProperty, property);
        ConventionManager.ApplyStringFormat(binding, convention, property);
        ConventionManager.ApplyValidation(binding, viewModelType, property);
        ConventionManager.ApplyUpdateSourceTrigger(bindableProperty, element, binding, property);

        if (Nullable.GetUnderlyingType(property.PropertyType) != null)
        {
            binding.TargetNullValue = String.Empty;
        }

        BindingOperations.SetBinding(element, bindableProperty, binding);
    };

    // Your own configurations... 
}

As you can see this code sets the TargetNullValue property of the Binding if the source property is Nullable. It is applied to all automatic binding that CM creates for you in your application.

I hope it helps.

EDIT

About your last comment, consider that Caliburn Micro do not handle FrameworkElements which are already binded (in the XAML) to a property when it applies the "automatic binding".

So if you want to automatically set the TargetNullValue of your "manual" bindings, well the simpliest idea is to create your own binding:

public class NullableBinding : Binding
{
    public NullableBinding(string path)
        : base(path)
    {
        TargetNullValue = String.Empty;
    }

    public NullableBinding()
        : base()
    {
        TargetNullValue = String.Empty;
    }
}

and then use it in your XAML:

<DataGridTextColumn Header="Rate" Width ="*" Binding="{local:NullableBinding rate}"/>
Il Vic
  • 5,576
  • 4
  • 26
  • 37
  • 1
    It did. The official answer from https://github.com/Caliburn-Micro/Caliburn.Micro/issues/448 didn't. Even when I changed `type` to `GetType()`. – Sabyasachi Mukherjee Jun 08 '17 at 09:48
  • Okay, tinkered with the code and finally this was working: `var existingApplyValidation = ConventionManager.ApplyValidation; ConventionManager.ApplyValidation = (binding, viewModelType, property) => { existingApplyValidation(binding, viewModelType, property); // preserve existing behavior if (Nullable.GetUnderlyingType(property.PropertyType) != null) binding.TargetNullValue = String.Empty; };` Your code helped. Copied the conditional statement from your code. – Sabyasachi Mukherjee Jun 08 '17 at 09:52
  • I was testing this solution, and it only works where Caliburn.Micro automatically binds the elements. For example it works here: ``. But it does not work where binding is manually defined, like: ``. Any way we can set the default TargetNullValue for application-wide bindings? – Sabyasachi Mukherjee Jun 09 '17 at 06:41
  • Thanks. That took care of it. – Sabyasachi Mukherjee Jun 09 '17 at 09:30
0

Well, customizing the binding in ConventionManagerof Caliburn was the way to go.

Here is the solution: https://github.com/Caliburn-Micro/Caliburn.Micro/issues/448