简体   繁体   中英

Bind object to ConverterParameter fails with XamlParseException in WP8.1 Silverlight

Initial situation

I have a Windows Phone 8.1 Silverlight app where I have a model that contains several properties as shown below (just an excerpt of 3 properties, it has a lot more).

public class WorkUnit : INotifyPropertyChanged
{
    public DateTime? To
    {
        get { return Get<DateTime?>(); }
        set
        {
            Set(value);
            OnPropertyChanged("To");
            OnPropertyChanged("ToAsShortTimeString");
        }
    }
    public string ToAsShortTimeString
    {
        get 
        { 
            if (To.HasValue)
            {
                if (Type == WorkUnitType.StartEnd)
                    return To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);

                var duration = To.Value - From;
                return DateHelper.FormatTime(duration, false);
            }

            return null;
        }
    }
    public short? Type
    {
        get { return Get<short?>(); }
        set 
        { 
            Set(value); 
            OnPropertyChanged("Type");
        }
    }
}

I'm using MVVMLight. There are several work units in an ObservableCollection that is bound to a list box on a Windows Phone page. The collection itself is part of a (WorkDay) view model which in turn is bound to the page itself.

What I want to do

I have a lot of properties in my model that are just used to format some properties for the UI. One such is ToAsShortTimeString which returns the time given by the To property, depending on the Type and the From properties, formatted as string.

In order to clean up my model I want to remove such formatter-properties as much as possible and use converters (IValueConverter) as much as possible. One further reason to move away from such properties is that the database that I use (iBoxDB) doesn't have member attributes like [Ignore] that is available for SQLite. So all properties with supported types are stored in the database. However, such formatter properties shouldn't be stored if possible.

What I did - 1st try

I now transformed all properties to converters and most of the time this was no problem. However, ToAsShortTimeString not just uses one property but 3 to format the input. Therefore, in XAML I need to provide either those 3 properties to the value converter or the work unit itself which is bound to the page.

public class WorkUnitToEndTimeStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var workUnit = (WorkUnit) value;
        if (workUnit.To.HasValue)
        {
            if (workUnit.Type == WorkUnitType.StartEnd)
                return workUnit.To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);

            var duration = workUnit.To.Value - workUnit.From;
            return DateHelper.FormatTime(duration, false);
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

So I changed the binding of the Text property in the TextBlock that shows the formatted To property to the WorkUnit that is bound to the page.

<TextBlock 
    Grid.Column="2" Grid.Row="0" 
    Grid.ColumnSpan="2" 
    Text="{Binding WorkUnit,Converter={StaticResource WorkUnitToEndTimeStringConverter}}" 
    FontSize="28" 
    FontFamily="Segoe WP Light" 
    Foreground="{StaticResource TsColorWhite}"/>

Unfortunately, when the To property changes in the model, even though OnPropertyChanged is called (see model code above), the text block doesn't get updated. I assume the reason is that only those controls are updated where some property is directly bound to the changed model property.

What I did - 2nd try

So as I need 3 properties from WorkUnit in order to correctly format To I changed the binding as follows. I bound Text to WorkUnit.To and set the ConverterParameter to the WorkUnit itself. With this change I hoped that whenever To is changed in the model and the value converter is called, I can format the time because I have all the info provided from the converter parameter (WorkUnit). (I'm not printing the updated converter here but I changed it to accomodate the change on the value and parameter input parameters)

<TextBlock 
    Grid.Column="2" Grid.Row="0" 
    Grid.ColumnSpan="2" 
    Text="{Binding WorkUnit.To,Converter={StaticResource WorkUnitToEndTimeStringConverter},ConverterParameter={Binding WorkUnit}}" 
    FontSize="28" 
    FontFamily="Segoe WP Light" 
    Foreground="{StaticResource TsColorWhite}"/>

Unfortunately, in this case a XamlParseException exception is thrown.

{System.Windows.Markup.XamlParseException: Failed to assign to property 'System.Windows.Data.Binding.ConverterParameter'. [Line: 61 Position: 18] ---> System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at MS.Internal.XamlManagedRuntimeRPInvokes.TryApplyMarkupExtensionValue(Object target, XamlPropertyToken propertyToken, Object value)
   at MS.Internal.XamlManagedRuntimeRPInvokes.SetValue(XamlTypeToken inType, XamlQualifiedObject& inObj, XamlPropertyToken inProperty, XamlQualifiedObject& inValue)
   --- End of inner exception stack trace ---
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)}

Question

So is there a way to remove the formatter-property from my model so that I can keep my model as clean as possible? Is there sth. wrong with my converter? Is there any other way that I currently don't know of?

You could have a property in your WorkUnit called EndTimeString

public string EndTimeString
{
    get
    { 
        string endTime = null;

        if (this.To.HasValue)
        {
            if (this.Type == WorkUnitType.StartEnd)
            {
                endTime = this.To.Value.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);
            }
            else
            {
                var duration = this.To.Value - this.From;
                endTime = DateHelper.FormatTime(duration, false);
            }        
        }

        return endTime
    }
}

Of course, if To value changes, you want to notify the UI that the EndTimeString has also changed so that it picks up the new value:

public DateTime? To
{
    get { return Get<DateTime?>(); }
    set
    {
        Set(value);
        OnPropertyChanged("To");
        OnPropertyChanged("EndTimeString");
    }
}

And then just bind straight to that string:

...Text="{Binding WorkUnit.EndTimeString}" />

Unfortunately you can't bind to parameters.

This would be easy with MultiBinding but that's not available for Windows Phone (as you stated in your comment).

You could implement it yourself but if you are not really into it :), there are implementations trying to mimic this behaviour. One of them can be found from the joy of code .

There is a NuGet package called Krempel's WP7 Library which has above implemented for WP7 but it works on WP8.x as well.

Downside is that it can only bind to elements in visual tree, so you have to use (for lack of a better word) relaying UI elements to get the job done. I have used similar technique myself when I can't bind directly to a property I need to. One case is AppBar , you can't bind directly to enabled property, so instead I use something like

<CheckBox Grid.Row="0" IsEnabled="{Binding AppBarEnabled}" 
          IsEnabledChanged="ToggleAppBar" Visibility="Collapsed" />

Anyway, below is full example, without any groovy patterns, on how you can achieve multibinding using above library. Try it out and see if it's worth the trouble. Options are that you have "extra" properties in your model or some extra elements and complexity in your view.

I used your WorkUnit model and Converter to make it more useful and easier to understand.

Outcome should be something like this.

在此处输入图片说明


MainWindow.xaml

<phone:PhoneApplicationPage
    x:Class="WP8MultiBinding.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="clr-namespace:Krempel.WP7.Core.Controls;assembly=Krempel.WP7.Core"
    xmlns:conv="clr-namespace:WP8MultiBinding"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    DataContext="{Binding RelativeSource={RelativeSource Self}}"
    shell:SystemTray.IsVisible="True">

    <phone:PhoneApplicationPage.Resources>
        <conv:WorkUnitToEndTimeStringConverter 
              x:Key="WorkUnitToEndTimeStringConverter" />
    </phone:PhoneApplicationPage.Resources>

    <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="80" />
            <RowDefinition Height="80" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <!-- Multibinding & converter -->
        <controls:MultiBinding 
            x:Name="MultiBinding" 
            Converter="{StaticResource WorkUnitToEndTimeStringConverter}"
            NumberOfInputs="3"
            Input1="{Binding ElementName=Type, Path=Text, Mode=TwoWay}"
            Input2="{Binding ElementName=From, Path=Text, Mode=TwoWay}"
            Input3="{Binding ElementName=To, Path=Text, Mode=TwoWay}" />

        <!-- Output from multibinded conversion -->
        <TextBox Text="{Binding ElementName=MultiBinding, Path=Output}" Grid.Row="0" />
        <!-- Update WorkUnit properties -->
        <Button Click="UpdateButtonClick" Grid.Row="1">Test MultiBinding</Button>

        <!-- Helper elements, might want to set visibility to collapsed -->
        <StackPanel HorizontalAlignment="Center" Grid.Row="2">
            <TextBlock x:Name="Type" Text="{Binding WorkUnit.Type, Mode=TwoWay}" />
            <TextBlock x:Name="From" Text="{Binding WorkUnit.From, Mode=TwoWay}" />
            <TextBlock x:Name="To" Text="{Binding WorkUnit.To, Mode=TwoWay}" />
        </StackPanel>
    </Grid>    
</phone:PhoneApplicationPage>

MainWindow.xaml.cs

using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using Microsoft.Phone.Controls;

namespace WP8MultiBinding
{
    public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            WorkUnit = new WorkUnit()
                {
                    To = DateTime.Now.AddHours(5),
                    From = DateTime.Now,
                    Type = WorkUnitType.StartEnd
                };
        }

        public WorkUnit WorkUnit { get; set; }

        // Ensure bindings do update
        private void UpdateButtonClick(object sender, RoutedEventArgs e)
        {
            WorkUnit.Type = WorkUnit.Type == WorkUnitType.StartEnd ? 
                            WorkUnit.Type = WorkUnitType.Other : 
                            WorkUnit.Type = WorkUnitType.StartEnd;

            WorkUnit.From = WorkUnit.From.AddMinutes(60);
            if (WorkUnit.To.HasValue)
                WorkUnit.To = WorkUnit.To.Value.AddMinutes(30);
        }
    }

    public enum WorkUnitType
    {
        StartEnd,
        Other
    }

    public class WorkUnit : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private WorkUnitType _type;
        private DateTime _from;
        private DateTime? _to;

        public WorkUnitType Type
        {
            get { return _type; }
            set { _type = value; OnPropertyChanged(); }
        }

        public DateTime From
        {
            get { return _from; }
            set { _from = value; OnPropertyChanged(); }
        }

        public DateTime? To
        {
            get { return _to; }
            set { _to = value; OnPropertyChanged(); }
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) 
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    // Multivalue Converter
    public class WorkUnitToEndTimeStringConverter : Krempel.WP7.Core.Controls.IMultiValueConverter
    {
        private const string DateFormat = "M/d/yyyy h:mm:ss tt";

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            // Index: 0 = Type, 1 = From, 2 = To
            if (values[2] != null)
            {
                var type = (WorkUnitType) Enum.Parse(typeof (WorkUnitType), values[0].ToString());
                var from = DateTime.ParseExact(values[1].ToString(), DateFormat, CultureInfo.InvariantCulture);
                var to = DateTime.ParseExact(values[2].ToString(), DateFormat, CultureInfo.InvariantCulture);

                if (type == WorkUnitType.StartEnd)
                    return to.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern);

                var duration = to - from;
                return duration; // DateHelper.FormatTime(duration, false);
            }

            return null;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM