在WP8.1 Silverlight中,使用XamlParseException将对象绑定到ConverterParameter失败

时间:2014-10-16 20:18:48

标签: c# xaml silverlight windows-phone windows-phone-8.1

初始情况

我有一个Windows Phone 8.1 Silverlight应用程序,其中我有一个包含多个属性的模型,如下所示(只有3个属性的摘录,它还有更多)。

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");
        }
    }
}

我正在使用MVVMLight。 ObservableCollection中有多个工作单元绑定到Windows Phone页面上的列表框。集合本身是(WorkDay)视图模型的一部分,而模型又与页面本身绑定。

我想做什么

我的模型中有很多属性,仅用于格式化UI的某些属性。其中一个是ToAsShortTimeString,它返回 To 属性给出的时间,具体取决于 Type From 属性,格式为字符串。

为了清理我的模型,我想尽可能地删除这些格式化程序属性,并尽可能使用转换器(IValueConverter)。远离这些属性的另一个原因是我使用的数据库(iBoxDB)没有像SQLite可用的[Ignore]等成员属性。因此,所有具有受支持类型的属性都存储在数据库中。但是,如果可能,不应存储此类格式化程序属性。

我做了什么 - 第一次尝试

我现在将所有属性转换为转换器,大部分时间这都没有问题。但是, ToAsShortTimeString 不仅使用一个属性而且使用3来格式化输入。因此,在XAML中,我需要向值转换器或绑定到页面的工作单元本身提供这3个属性。

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();
    }
}

所以我更改了TextBlock中 Text 属性的绑定,该属性将格式化的 To 属性显示给绑定到页面的WorkUnit。

<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}"/>

不幸的是,当模型中的 To 属性发生变化时,即使调用OnPropertyChanged(参见上面的模型代码),文本块也不会更新。我假设原因是只更新那些控件,其中某些属性直接绑定到更改的模型属性。

我做了什么 - 第二次尝试

因为我需要来自WorkUnit的3个属性才能正确格式化 To 我更改了绑定,如下所示。我将 Text 绑定到 WorkUnit.To 并将 ConverterParameter 设置为WorkUnit本身。有了这个改变,我希望无论何时在模型中更改 To 并调用值转换器,我都可以格式化时间,因为我拥有转换器参数(WorkUnit)提供的所有信息。 (我不在这里打印更新的转换器,但我更改了它以适应值和参数输入参数的更改)

<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}"/>

不幸的是,在这种情况下会抛出XamlParseException异常。

{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)}

问题

那么有没有办法从我的模型中删除formatter-property,以便尽可能让我的模型保持干净?是吗?我的转换器有问题吗?还有其他方式,我目前不知道吗?

2 个答案:

答案 0 :(得分:0)

您可以在WorkUnit 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
    }
}

当然,如果To值发生变化,您希望通知用户界面EndTimeString也已更改,以便它获取新值:

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

然后直接绑定到该字符串:

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

答案 1 :(得分:0)

不幸的是你无法绑定参数。

MultiBinding这很容易,但是Windows Phone无法使用(正如您在评论中所述)。

你可以自己实现它,但如果你真的没有实现它:),有些实现试图模仿这种行为。其中一个可以从the joy of code找到。

有一个名为Krempel's WP7 Library的NuGet包,其上面已经为WP7实现了,但它也适用于WP8.x。

缺点是它只能绑定到可视树中的元素,因此您必须使用(缺少更好的词)来转发UI元素以完成工作。当我无法直接绑定到我需要的属性时,我自己也使用了类似的技术。一个案例是AppBar,你不能直接绑定到启用的属性,所以我使用类似

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

无论如何,下面是完整的例子,没有任何groovy模式,关于如何使用上面的库实现多重绑定。尝试一下,看看是否值得麻烦。选项是您的模型中具有“额外”属性或视图中的一些额外元素和复杂性。

我使用了您的WorkUnit模型和转换器,使其更有用,更易于理解。

结果应该是这样的。

enter image description here


<强> 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();
        }
    }

}