0
votes

Can someone please shed some light on this error?

At first I thought SelectedIndex is probably just not a DependencyProperty and cannot be bound, but I was wrong.

If I use a normal binding instead of the markup extension src:ValidatedBinding, or if I keep the markup extension but bind SelectedItem instead of SelectedIndex, then it works.

Here is a small app to demonstrate the problem.

The main window:

<Window       x:Class="WpfApplication2.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:src="clr-namespace:WpfApplication2"
                Title="MainWindow" 
               Height="350" 
                Width="525"
                     >
    <ComboBox SelectedIndex="{src:ValidatedBinding SelectedIndex}"
              VerticalAlignment="Center" HorizontalAlignment="Center" Width="100">
        <ComboBoxItem>Not Specified</ComboBoxItem>
        <ComboBoxItem>First</ComboBoxItem>
        <ComboBoxItem>Second</ComboBoxItem>
    </ComboBox>
</Window>

The code behind the main window:

using System.Windows;

namespace WpfApplication2
{
    /// <summary>
    /// The main window.
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new Item { Description = "Item 1", SelectedIndex = 0 };
        }
    }

    /// <summary>
    /// An object with a string and an int property.
    /// </summary>
    public sealed class Item
    {
        int _selectedIndex;
        string _description;

        public string Description
        {
            get { return _description; }
            set { _description = value; }
        }

        public int SelectedIndex
        {
            get { return _selectedIndex; }
            set { _selectedIndex = value; }
        }
    }
}

The code for the markup extension:

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

namespace WpfApplication2
{
    /// <summary>
    /// Creates a normal Binding but defaults NotifyOnValidationError and 
    /// ValidatesOnExceptions to True, Mode to TwoWay and UpdateSourceTrigger
    /// to LostFocus.
    /// </summary>
    [MarkupExtensionReturnType(typeof(Binding))]
    public sealed class ValidatedBinding : MarkupExtension
    {
        public ValidatedBinding(string path)
        {
            Mode = BindingMode.TwoWay;

            UpdateSourceTrigger = UpdateSourceTrigger.LostFocus;

            Path = path;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var Target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));

            /* on combo boxes, use an immediate update and validation */
            DependencyProperty DP = Target.TargetProperty as DependencyProperty;
            if (DP != null && DP.OwnerType == typeof(System.Windows.Controls.Primitives.Selector)
                && UpdateSourceTrigger == UpdateSourceTrigger.LostFocus) {
                UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            }

            return new Binding(Path) {
                Converter = this.Converter,
                ConverterParameter = this.ConverterParameter,
                ElementName = this.ElementName,
                FallbackValue = this.FallbackValue,
                Mode = this.Mode,
                NotifyOnValidationError = true,
                StringFormat = this.StringFormat,
                ValidatesOnExceptions = true,
                UpdateSourceTrigger = this.UpdateSourceTrigger
            };
        }

        public IValueConverter Converter { get; set; }

        public object ConverterParameter { get; set; }

        public string ElementName { get; set; }

        public object FallbackValue { get; set; }

        public BindingMode Mode { get; set; }

        [ConstructorArgument("path")]
        public string Path { get; set; }

        public string StringFormat { get; set; }

        public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
    }
}

The exception when I run this application:

System.Windows.Markup.XamlParseException occurred
HResult=-2146233087 Message='Set property 'System.Windows.Controls.Primitives.Selector.SelectedIndex' threw an exception.' Line number '9' and line position '19'.
Source=PresentationFramework LineNumber=9 LinePosition=19
StackTrace: at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri) at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri) at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream) at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator) at WpfApplication2.MainWindow.InitializeComponent() in c:\Users\Administrator\Documents\Visual Studio 2012\Projects\WpfApplication2\MainWindow.xaml:line 1 at WpfApplication2.MainWindow..ctor() in c:\Users\Administrator\Documents\Visual Studio 2012\Projects\WpfApplication2\MainWindow.xaml.cs:line 12
InnerException: System.ArgumentException HResult=-2147024809 Message='System.Windows.Data.Binding' is not a valid value for property 'SelectedIndex'. Source=WindowsBase StackTrace: at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal) at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value) at System.Windows.Baml2006.WpfMemberInvoker.SetValue(Object instance, Object value) at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(XamlMember member, Object obj, Object value) at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(Object inst, XamlMember property, Object value) InnerException:

1
SelectedIndex is an int. You can't put a System.Windows.Data.Binding inside an int property. Your MarkupExtension is wrong. You're returning the Binding itself instead of evaluating it and returning the Binding Source.Federico Berasategui
@HighCore - But a normal {Binding xxx} is not an int either. I don't understand. If I were to use {Binding SelectedIndex} instead of {src:ValidatedBinding SelectedIndex} then it would work, and essentially both return a binding. Where am I missing the boat?Martin Lottering
The Binding markup extension does not return a Binding instance, it creates the binding and returns the value in the other end (the Source)Federico Berasategui
@HighCore - Thank you for the explanation. That makes sense, except the markup extension works on SelectedItem. Does SelectedItem disregard the binding and assign the value instead?Martin Lottering
SelectedItem is of type object, that's why you're not getting an exception, but that doesn't mean it's working. It's not, you're just assigning an instance of the Binding class to the SelectedItem property.Federico Berasategui

1 Answers

1
votes

Ok, here is a proxy binding that works if anyone is interested.

Thank you @HighCore for pointing me in the right direction.

I use this binding proxy to set non-standard default values on the binding so I don't have to set them everywhere. This makes my xaml more compact, and allows me to have a central place where I 'style' my bindings.

These are the defaults:

  • NotifyOnValidationError = True,
  • ValidatesOnExceptions = True,
  • Mode = TwoWay,
  • UpdateSourceTrigger = LostFocus for 'Text' properties, otherwise PropertyChanged.

The UpdateSourceTrigger will change to PropertyChanged if the target property is not a Text property. (e.g. Combos or CheckBoxes)

If I don't need validation, I use the normal binding:

<TextBlock Text="{Binding FirstName}" />

If I need a normal two way binding, I know I might need validation so I use this binding proxy:

<TextBox Text="{i:ValidatedBinding FirstName}" />

That means I don't have to write out:

<TextBox Text="{Binding FirstName
    , Mode=TwoWay
    , UpdateSourceTrigger=LostFocus
    , NotifyOnValidationError=True
    , ValidatesOnExceptions=True" />

It works on both SelectedItem (reference types) and SelectedIndex (value types).

It will monitor the DataContext and maintain the binding.

If you find holes in the code, fix bugs or have any suggestions, please let me know.

using ITIS.Reflection /* you can replace this with System.Reflection */;
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

namespace ITIS
{
    /// <summary>
    /// Creates a Binding with the following defaults:
    /// <para>- NotifyOnValidationError = True, </para>
    /// <para>- ValidatesOnExceptions = True, </para>
    /// <para>- Mode = TwoWay, </para> 
    /// <para>- UpdateSourceTrigger = LostFocus for 'Text' properties, otherwise PropertyChanged.</para>
    /// </summary>
#if !SILVERLIGHT
    [MarkupExtensionReturnType(typeof(Binding))]
#endif
    public sealed class ValidatedBinding : MarkupExtension
    {
        #region CONSTRUCTOR

        public ValidatedBinding(string path)
        {
            Mode = BindingMode.TwoWay;

            Path = path;

            /* possibly changed again in ProvideValue() */
            UpdateSourceTrigger = UpdateSourceTrigger.Default;
        }

        #endregion

        #region PROPERTIES

        public IValueConverter Converter { get; set; }

        public object ConverterParameter { get; set; }

        public string ElementName { get; set; }

        public object FallbackValue { get; set; }

        public BindingMode Mode { get; set; }

#if !SILVERLIGHT
        [ConstructorArgument("path")]
#endif
        public string Path { get; set; }

        public string StringFormat { get; set; }

        public UpdateSourceTrigger UpdateSourceTrigger { get; set; }

        #endregion

        #region FIELDS

        bool _bound;
        DependencyProperty _property;
        FrameworkElement _element;

        #endregion

        #region OPERATIONS

        void ClearBinding()
        {
            _element.ClearValue(_property);
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget Target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

            if (Target == null) {
                throw new InvalidOperationException(
                    "Cannot resolve the IProvideValueTarget. Are you binding to a property?");
            }

#if !SILVERLIGHT
            /* on text boxes, use a LostFocus update trigger */
            _property = Target.TargetProperty as DependencyProperty;

            if (_property != null) {
                if (_property.Name.StartsWith("Text")) {
                    UpdateSourceTrigger = UpdateSourceTrigger.LostFocus;
                }
                else {
                    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
                }
            }
#endif

            _element = Target.TargetObject as FrameworkElement;

            if (_element != null) {

                _element.DataContextChanged += Element_DataContextChanged_SetBinding;

                if (_element.DataContext != null || !string.IsNullOrWhiteSpace(ElementName)) {

                    SetBinding();

                    /* can be replaced with normal reflection PropertyInfo.GetValue() */
                    return FastReflector.GetPropertyValue(/* object = */ _element.DataContext, /* property name = */ Path);
                }

                /* don't return null for value types */
                if (_property.PropertyType.IsValueType) {
                    return Activator.CreateInstance(_property.PropertyType);
                }

                return null;
            }

            return this;
        }

        void SetBinding()
        {
            _bound = true;

            Binding Binding = new Binding() {
                Path = new PropertyPath(this.Path),
                Converter = this.Converter,
                ConverterParameter = this.ConverterParameter,
                FallbackValue = this.FallbackValue,
                Mode = this.Mode,
                NotifyOnValidationError = true,
                StringFormat = this.StringFormat,
                ValidatesOnExceptions = true,
                UpdateSourceTrigger = this.UpdateSourceTrigger
            };

            /* only set when necessary to avoid a validation exception from the binding */
            if (_element.DataContext != null) { Binding.Source = _element.DataContext; }
            if (!string.IsNullOrWhiteSpace(ElementName)) { Binding.ElementName = ElementName; }

            _element.SetBinding(_property, Binding);
        }

        #endregion

        #region EVENT HANDLERS

        void Element_DataContextChanged_SetBinding(object sender, DependencyPropertyChangedEventArgs e)
        {
            /* cleanup the old binding */
            if (_bound) { ClearBinding(); }

            SetBinding();
        }

        #endregion
    }
}