WPF/Silverlight/XAML中的强类型数据绑定?

29
我对XAML数据绑定的最大不满之一是没有强类型数据绑定选项。换句话说,在C#中,如果您想访问一个不存在的对象属性,Intellisense将无法提供任何帮助,如果您不理会Intellisense,编译器将会提示错误并阻止您继续进行--我认为在座的许多人都会认为这是一件好事。但是在XAML数据绑定中,您是在没有网的情况下操作。您可以绑定到任何东西,即使它不存在。实际上,鉴于XAML数据绑定的奇怪语法和我的经验,绑定到存在的东西比绑定到不存在的东西要复杂得多。我更有可能把我的数据绑定语法搞错而不是正确地使用它;与微软堆栈的任何其他部分(包括笨拙而烦人的WCF,如果你能相信)相比,我花费在排除XAML数据绑定问题上的时间很容易就超过了其他时间。其中大部分(不是全部)归因于没有强类型数据绑定,因此我无法从Intellisense或编译器获得任何帮助。
所以我想知道的是:为什么微软至少不给我们提供一种选项,可以获得强类型数据绑定:就像在VB6中,如果我们真的想自虐,可以将任何对象都变成一个variant,但大多数情况下使用普通类型的变量更有意义。微软难道不能这样做吗?
这是一个例子,如果C#中的属性"UsrID"不存在,您将从Intellisense获得警告,并且如果尝试此操作,则会从编译器获得错误提示。
string userID = myUser.UsrID;

但是在XAML中,您可以随心所欲地进行这些操作:

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

Intellisense、编译器和应用本身在运行时都不会给你任何提示,表明你做错了什么。尽管这只是一个简单的例子,但任何处理复杂对象图和复杂 UI 的实际应用程序都会有很多类似的场景,这些场景并不简单,也不容易进行故障排除。即使第一次正确地实现了它,如果重构代码并更改了 C# 属性名称,则问题就来了。一切都将编译,并且没有错误,但什么也不起作用,你只能费力搜寻整个应用程序,试图找出哪里出了问题。

我的一个可能的建议(只是脑海中的想法,我还没有深思熟虑)可能是这样的:

对于逻辑树的任何部分,您可以在 XAML 中指定它所期望的对象的 DataType,如下所示:

<Grid x:Name="personGrid" BindingDataType="{x:Type collections:ObservableCollection x:TypeArgument={data:Person}}">

这可能会在 .g.cs 文件中生成一个强类型的 ObservableCollection<Person> TypedDataContext 属性。因此,在您的代码中:

// This would work
personGrid.TypedDataContext = new ObservableCollection<Person>(); 

// This would trigger a design-time and compile-time error
personGrid.TypedDataContext = new ObservableCollection<Order>(); 

如果您通过网格上的控件访问那个 TypedDataContext,它将知道您正在尝试访问哪种类型的对象。

<!-- It knows that individual items resolve to a data:Person -->
<ListBox ItemsSource="{TypedBinding}">
    <ListBox.ItemTemplate>
       <DataTemplate>
           <!--This would work -->
           <TextBlock Text="{TypedBinding Path=Address.City}" />
           <!-- This would trigger a design-time warning and compile-time error, since it has the path wrong -->
           <TextBlock Text="{TypedBinding Path=Person.Address.City} />
       </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

我写了一篇博客文章(在这里),更详细地解释了我对WPF / XAML数据绑定的挫败感,并提出了一个显著更好的方法。有没有什么理由它不能起作用?是否有人知道微软是否计划修复这个问题(按照我的建议,或者希望是更好的建议)?


4
你的博客文章抱怨了缺乏绑定到强类型对象的支持(你正在使用XML作为静态资源进行绑定)。实际上,绑定到强类型对象是WPF的主要功能之一,所以我不太确定你在抱怨什么。 - Janie
4
是的,你可以绑定到强类型对象。但是绑定本身在设计或编译时不是强类型的。换句话说,如果属性“UsrID”不存在,C# 将不允许我这样做:string userID = myUser.UsrID;但是如果我想在 XAML 中做同样的事情,(a) 它不会给我任何 IntelliSense 提示表明我做错了;(b) 当我编译我的应用程序时,它不会告诉我我正在做错;并且 (c) 最令人惊讶的是,即使当我运行我的应用程序时,它也不会告诉我我正在做错。它只是静默地失败。 - Ken Smith
1
通常情况下,我会同意你的观点,在VS的任何开发体验中缺乏Intellisense支持确实会让你感到困扰。 但是,在这种情况下;一旦将特定UI部分的数据上下文绑定到CLR对象,就真的不难回溯并验证属性名称是否匹配。你最初的抱怨似乎来自尝试绑定到XML静态资源... - Janie
1
不是你的问题需要更清晰,而是你忽略了视图与你要绑定的类型之间缺乏耦合实际上是一种特性。你正在寻找的是类似于ASP.NET MVC提供的东西,可以使用Page:ViewBase<T>,其中T是您打算为其创建视图的类型。这样做没问题,但肯定有局限性。除非它们共享继承的公共点,否则无法将视图应用于两种不同的类型。这使得远离了WPF无限可组合的目标。这就是为什么你得到了负投票的原因。 - Anderson Imes
7
这里有另一种表述方式。当我们必须在不同的技术集群之间进行接口,例如从C#调用SQL或从Silverlight调用JavaScript时,我们都习惯于失去Intellisense/strong typing。对我来说,这就像XAML的感觉。但它不应该是这样的。是的,C#和XAML之间应该明确分离关注点:但它们都被设计为在公共类型系统中工作,因此它们都应该使用相同的类型,特别是数据绑定。 - Ken Smith
显示剩余6条评论
10个回答

11

在Visual Studio 2010中,将会有数据绑定的IntelliSense支持。这似乎是你投诉的核心问题,因为数据绑定强类型的。只是在运行时才知道绑定是否成功,往往它默默地失败而不是抛出异常。如果绑定失败,WPF会通过调试跟踪转储解释文本,你可以在Visual Studio输出窗口中看到。

除了缺少IntelliSense支持和一些奇怪的语法问题外,数据绑定做得相当不错(至少在我看来)。如果需要更多帮助调试数据绑定,建议查看Bea的精彩文章这里


我还没有使用过VS 2010,但你知道它如何工作吗?我还没有读到任何表明它会解决我上面所述问题的东西,即它会强制我声明我想绑定到的对象的类型,然后告诉我是否不当地进行了绑定。但是像我之前说的,我还没有使用过它。 - Ken Smith
3
顺便说一下,我已经读了Bea的文章很多遍:这是我到目前为止唯一的生存之道 :-). 但我的观点是,Bea概述的方式“不应该被需要”。如果数据绑定做得正确,所有那些烦人的故障排查工作应该由编译器和IDE完成,而不是开发人员。 - Ken Smith
我非常尊重地不同意。你没看过蜘蛛侠吗?“伴随强大的力量而来的,是巨大的责任。”像数据绑定这样强大的功能需要付出很多努力才能熟练掌握。但正因为数据绑定如此灵活且能为你做许多事情,所以调试起来通常会有一些困难。 - Charlie
1
也许这就是你和我意见不合的地方。在VB6中,我从未使用过Variant数据类型,因为它是一种不良实践——无论它给我分配变量的能力有多大。一个设计良好的语言不可或缺的特征之一,就是它明确禁止我做某些事情。如果我的操作会出问题,我更愿意让编译器告诉我,而不是让用户遭殃。 - Ken Smith
1
我有Visual Studio 2010,我所见过的关于绑定的智能感知支持仅限于它允许您从“Binding”类的属性中进行选择。我和OP想要的是针对“DataContext”上的属性的智能感知。为了获得智能感知支持和编译时检查,我不介意在我的XAML文件中的某个地方指定“DataContext”的类型。如果我选择不指定以允许更多的自由,那也应该被允许。但有时候,我们需要的是控制而不是自由。 - devuxer

9
这是我对XAML最大的抱怨!没有编译器强制执行有效的数据绑定是一个很大的问题。我并不在意智能感知,但我确实关心缺乏重构支持。
在WPF应用程序中更改属性名称或类型是危险的 - 使用内置的重构支持不会更新XAML中的数据绑定。在名称上进行搜索和替换是危险的,因为它可能会更改您没有打算更改的代码。查找结果列表很麻烦且耗时。
MVC已经有了强类型视图 - MVC contrib项目为MVC1提供了这些视图,而MVC2则提供了本地支持。XAML必须在未来支持此功能,特别是在“敏捷”项目中使用,其中应用程序的设计随着时间的推移而发展。我还没有看过.NET 4.0 / VS2010,但我希望体验比现在好得多!

6

我认为XAML有点像早期的HTML。我无法想象10多年后,我仍然要手动键入开闭标签,因为我无法拥有一个成熟的GUI来定义样式、绑定和模板。我完全支持你的观点,Ken。我感到非常奇怪,为什么这么多人支持MVVM,却没有一个人对调试XAML的痛苦提出任何抱怨。命令绑定和数据绑定是非常好的概念,我一直在设计我的Winform应用程序时采用这种方式。然而,XAML绑定解决方案以及其他一些XAML问题(或缺乏复杂的功能)真的是VS中的一个巨大失败。它使得开发和调试变得非常困难,并使代码非常难以阅读。


5
肯,C#需要一个简洁的语法元素来引用PropertyInfo类。PropertyInfo结构是在编译时定义的静态对象,因此为对象上的每个属性提供了唯一的键。可以在编译时验证属性。但唯一的问题是将对象实例视为数据类型,这有些奇怪,因为强类型是对类型而不是类型的值进行强制执行的。传统上,编译器不强制执行数据值,而是依赖于运行时代码来检查其数据。大多数情况下,甚至不可能在编译时验证数据,但反射是其中之一,至少在这种情况下是可能的。
或者,编译器可以为每个属性创建一个新的数据类型。我可以想象会创建很多类型,但这将使属性绑定在编译时得到执行。
从某种意义上说,CLR引入了一种反射级别,与之前的系统相比,这是另一种数量级别的反射。现在它被用来做一些相当令人印象深刻的事情,例如数据绑定。但它的实现仍然处于元数据级别,即编译器为每个数据类型生成的报告。我想,让C#发展的一种方式就是将元数据提升到编译时检查。
似乎有人可以开发一个编译工具,添加反射级别的验证。新的智能感应有点像这样。要通用地发现将来将与PropertyInfos进行比较的字符串参数可能有些棘手,但并非不可能。可以定义一个名为“PropertyString”的新数据类型,清楚地标识将来将与PropertyInfos进行比较的参数。
无论如何,我理解你的痛苦。我曾经追踪过很多拼写错误的属性名称引用。老实说,与反射相关的WPF存在许多令人烦恼的问题。一个有用的工具是WPF执行检查器,它确保所有静态控件构造函数都在正确位置,属性正确定义,绑定准确,正确的密钥等等。可以执行长列表的验证。
如果我还在Microsoft工作,我可能会尝试做到这一点。

1
谢谢你能理解我的痛苦,Grant。这里似乎没有其他人能够理解我为什么认为微软的实现有问题,所以我感到孤独。尽管有其他不同的观点,但我仍然坚信XAML数据绑定存在严重问题。最近我遇到的一个经典例子是在尝试重新合并分支时出现的。在C#中,如果您想要搞砸一个分支合并,那是相当困难的,因为编译器会发出警告。但是,如果底层数据类型已更改,则在进行完整的QA测试之前,您将不知道XAML是否已损坏:即使自动化测试通常也无法捕获这些错误。 - Ken Smith
WPF 不应该如此严重地依赖反射。泛型会更好。 - Scover
WPF在反射上过于依赖是不应该的。泛型会更好一些。 - undefined

2

听起来你所要求的不需要进行任何框架更改,可能已经在VS 2010中得到解决(我没有安装它)。

当您指定DataType时,XAML设计器需要对DataTemplate内的绑定进行智能感知,可以像这样实现:

<DataTemplate DataType="{x:Type data:Person}">
...
</DataTemplate>

我同意在这种情况下拥有智能感知会是一个有帮助的变化,但您的其他建议似乎忽略了在运行时更改DataContexts并使用DataTemplates以独特方式呈现不同类型的重点。


好的,关于DataTemplates的观点很好。我同意这是一个有价值的场景。但是在使用单独的datatemplates并在运行时在它们之间进行选择的许多场景中,这可能过于复杂了。在那些场景中,我仍然认为能够在XAML树的某个级别上声明我的类型是有价值的。如果我设计的表单只适用于一种类型,我想在XAML中指定它,以便我可以立即清楚地知道何时出错。 - Ken Smith
2
你的回答在事实上是正确的,但我认为仍然应该可能强制对视图进行强类型化。就像在ASP.NET中一样。目前XAML的工作方式相当于使用dynamic关键字在C#中完成所有操作。拥有灵活和动态的能力很棒,但拥有强制执行强类型化的能力也很棒。为什么我们不能两者兼备呢? - devuxer

2

这确实是您所需要的解决方案!

但是,它不是一个内置的、本地的框架解决方案。是的,我认为这就是我们所有人真正想要的。也许我们以后会得到这个。

在此期间,如果您一心想限制类型,那么这可以解决问题!

使用这个转换器:

public class RequireTypeConverter : System.Windows.Data.IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return value;

        // user needs to pass a valid type
        if (parameter == null)
            System.Diagnostics.Debugger.Break();

        // parameter must parse to some type
        Type _Type = null;
        try
        {
            var _TypeName = parameter.ToString();
            if (string.IsNullOrWhiteSpace(_TypeName))
                System.Diagnostics.Debugger.Break();
            _Type = Type.GetType(_TypeName);
            if (_Type == null)
                System.Diagnostics.Debugger.Break();
        }
        catch { System.Diagnostics.Debugger.Break(); }

        // value needs to be specified type
        if (value.GetType() != _Type)
            System.Diagnostics.Debugger.Break();

        // don't mess with it, just send it back
        return value;
    }

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

然后,像这样限制您的数据类型:
<phone:PhoneApplicationPage.Resources>

    <!-- let's pretend this is your data source -->
    <CollectionViewSource x:Key="MyViewSource" Source="{Binding}"/>

    <!-- validate data type - start -->
    <converters:RequireTypeConverter x:Key="MyConverter" />
    <TextBlock x:Key="DataTypeTestTextBlock" 
        DataContext="{Binding Path=.,
            Source={StaticResource MyViewSource},
            Converter={StaticResource MyConverter}, 
            ConverterParameter=System.Int16}" />
    <!-- validate data type - end -->

</phone:PhoneApplicationPage.Resources>

请看我如何要求CollectionViewSource具有System.Int16的属性?当然,你甚至不能将CVS的源设置为整数,因此这总是会失败的。但这确实证明了这一点。只可惜Silverlight不支持{x:Type},否则我可以做一些类似于ConverterParameter={x:Type sys:Int16}的东西,那就太好了。
另外,XAML应该是非侵入式的,所以您应该能够在没有任何风险的情况下实现它。如果数据类型不是您想要的类型,调试器就会停止工作,您就可以为自己打破规则而感到遗憾。
再次说明,我知道这有点奇怪,但它确实可以实现您想要的效果 - 实际上,在编写代码时我一直在尝试并使用它,也许我甚至已经找到了用途。我喜欢它仅在设计时间/调试时使用。但是,请注意,我不是在向您推销它。
我只是玩得开心,如果这太复杂了,请享受我的努力;)
PS:您还可以创建一个类型为Type的附加属性,可以以相同的方式使用。您可以将其附加到您的CVS或任何其他内容,例如x:RequiredType="System.Int16",并在属性的行为中重复转换器逻辑。相同的效果,可能需要相同数量的代码 - 但如果您是认真的,这是另一个可行的选项。

1
还不错。我创建了一个名为“DebugConverter”的转换器,类似地使用它来至少掌握绑定发生的情况,但设置起来比应该的更繁琐,尽管它有所帮助。这个方法或多或少将其提升到了下一个级别。我也喜欢你提出的附加属性“RequiredType”的想法。我得调查一下。 - Ken Smith

1

好的,我忍不住了,这里是附加属性的方法:

这是XAML代码:

<phone:PhoneApplicationPage.Resources>

    <!-- let's pretend this is your data source -->
    <CollectionViewSource 
        x:Key="MyViewSource" Source="{Binding}"
        converters:RestrictType.Property="Source"                  
        converters:RestrictType.Type="System.Int16" />

</phone:PhoneApplicationPage.Resources>

这是属性代码:

public class RestrictType
{
    // type
    public static String GetType(DependencyObject obj)
    {
        return (String)obj.GetValue(TypeProperty);
    }
    public static void SetType(DependencyObject obj, String value)
    {
        obj.SetValue(TypeProperty, value);
        Watch(obj);
    }
    public static readonly DependencyProperty TypeProperty =
        DependencyProperty.RegisterAttached("Type",
        typeof(String), typeof(RestrictType), null);

    // property
    public static String GetProperty(DependencyObject obj)
    {
        return (String)obj.GetValue(PropertyProperty);
    }
    public static void SetProperty(DependencyObject obj, String value)
    {
        obj.SetValue(PropertyProperty, value);
        Watch(obj);
    }
    public static readonly DependencyProperty PropertyProperty =
        DependencyProperty.RegisterAttached("Property",
        typeof(String), typeof(RestrictType), null);

    private static bool m_Watching = false;
    private static void Watch(DependencyObject element)
    {
        // element must be a FrameworkElement
        if (element == null)
            System.Diagnostics.Debugger.Break();

        // let's not start watching until each is set
        var _PropName = GetProperty(element);
        var _PropTypeName = GetType(element);
        if (_PropName == null || _PropTypeName == null)
            return;

        // we will not be setting this up twice
        if (m_Watching)
            return;
        m_Watching = true;

        // listen with a dp so it is a weak reference
        var _Binding = new Binding(_PropName) { Source = element };
        var _Prop = System.Windows.DependencyProperty.RegisterAttached(
            "ListenToProp" + _PropName,
            typeof(object), element.GetType(),
            new PropertyMetadata((s, e) => { Test(s); }));
        BindingOperations.SetBinding(element, _Prop, _Binding);

        // run now in case it is already set
        Test(element);
    }

    // test property value type
    static void Test(object sender)
    {
        // ensure element type (again)
        var _Element = sender as DependencyObject;
        if (_Element == null)
            System.Diagnostics.Debugger.Break();

        // the type must be provided
        var _TypeName = GetType(_Element);
        if (_TypeName == null)
            System.Diagnostics.Debugger.Break();

        // convert type string to type
        Type _Type = null;
        try
        {
            _Type = Type.GetType(_TypeName);
            if (_Type == null)
                System.Diagnostics.Debugger.Break();
        }
        catch { System.Diagnostics.Debugger.Break(); }

        // the property name must be provided
        var _PropName = GetProperty(_Element);
        if (string.IsNullOrWhiteSpace(_PropName))
            System.Diagnostics.Debugger.Break();

        // the element must have the specified property
        var _PropInfo = _Element.GetType().GetProperty(_PropName);
        if (_PropInfo == null)
            System.Diagnostics.Debugger.Break();

        // the property's value's Type must match
        var _PropValue = _PropInfo.GetValue(_Element, null);
        if (_PropValue != null)
            if (_PropValue.GetType() != _Type)
                System.Diagnostics.Debugger.Break();
    }
}

祝你好运!只是玩乐而已。

现在很酷的是你可以限制任何属性的数据类型。不确定这有多有用,但现在你可以了! - Jerry Nixon
我在这里感觉有点傻。我现在看到你要求的是WPF,而我写的是整个Silverlight。它不仅仅是一对一,但移植过去只需要几分钟。 - Jerry Nixon
没问题 - 我现在几乎所有的工作都是用 Silverlight 做的 :-). - Ken Smith

0
大家好, Ken和Grant想说的是..。
我能不能有一个XAML,可以像p.UserId)>这样做,其中P是类型为Customer的DataContext?

如果我们有强类型数据上下文的选项,我们将能够在这些情况下获得智能感知,这将非常有帮助。 - Ken Smith
2
@ken smith 我在 ASP.NET MVC 视图中见过这种情况发生... 微软可以在 XAML 中提供相同的支持。 - satish

0
此外,如果您在调试项目时打开了输出窗口,VS会通知您任何数据绑定错误,例如控件绑定的属性不存在。

这是真的 - 虽然我明确认为它应该抛出异常,而不仅仅是打印调试错误。但是,我发现了许多其他情况,其中WPF遇到致命的绑定错误,并且默默失败,甚至没有打印调试消息(除非您通过启用详细跟踪来进行操作)。糟糕的默认值!糟糕的默认值!没有饼干! :-) - Ken Smith

0

我经常使用那种方法,它很有帮助,但并不完全一样。我想要的是能够在XAML中指定特定类型,这意味着“不要让我将任何与此类型不匹配的内容绑定到此(部分)控件上。如果我尝试这样做,或者如果我指定了一个无法与该类型配合的绑定,请在设计时、编译时和运行时提醒我。” - Ken Smith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接