28
votes

One of my biggest pet peeves with how databinding works with XAML is that there's no option to strongly type your databindings. In other words, in C#, if you want to access a property on an object that doesn't exist, you won't get any help from Intellisense, and if you insist on ignoring Intellisense, the compiler will gripe at you and won't let you proceed -- and I suspect that lots of folks here would agree that this is a Very Good Thing. But in XAML databinding, you're operating without a net. You can bind to anything, even if it doesn't exist. Indeed, given the bizarre syntax of XAML databinding, and given my own experience, it's a great deal more complicated to bind to something that does exist than to something that doesn't. I'm much more likely to get my databinding syntax wrong than to get it right; and the comparative time I spend troubleshooting XAML databindings easily dwarfs the time I spend with any other portion of Microsoft's stack (including the awkward and annoying WCF, if you can believe it). And most of that (not all of it) goes back to the fact that without strongly-typed databindings, I can't get any help from either Intellisense or the compiler.

So what I want to know is: why doesn't MS at least give us an option to have strongly-typed databindings: kind of like how in VB6, we could make any object a variant if we were really masochistic, but most of the time it made sense to use normal, typed variables. Is there any reason why MS couldn't do that?

Here's an example of what I mean. In C#, if the property "UsrID" doesn't exist, you'll get a warning from Intellisense and an error from the compiler if you try this:

string userID = myUser.UsrID;

However, in XAML, you can do this all you want:

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

And neither Intellisense, the compiler, or (most astonishingly) the application itself at runtime will give you any hint that you've done something wrong. Now, this is a simplistic example, but any real-world application that deals with complex object graphs and complex UI's is going to have plenty of equivalent scenarios that aren't simple at all, nor simple to troubleshoot. And even after you've gotten it working correctly the first time, you're SOL if you refactor your code and change your C# property names. Everything will compile, and it'll run without an error, but nothing will work, leaving you to hunt and peck your way through the entire application, trying to figure out what's broken.

One possible suggestion (off the top of my head, and which I haven't thought through) would maybe be something like this:

For any portion of the logical tree, you could specify in XAML the DataType of the object that it's expecting, like so:

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

This would perhaps generate a strongly-typed ObservableCollection<Person> TypedDataContext property in the .g.cs file. So in your code:

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

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

And if you then accessed that TypedDataContext through a control on the grid, it would know what sort of an object you were trying to access.

<!-- 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>

I've made a blog posting here that explains more about my frustrations with WPF/XAML databinding, and what I think would be a significantly better approach. Is there any reason why this couldn't work? And does anyone know if MS is planning to fix this problem (along the lines of my proposal, or hopefully, a better one)?

10
Your blogpost complained about the lack of support of binding to strongly typed objects (you were using XML as a staticresource to bind against.) Binding to strongly typed objects is actually one of the major features that comes with WPF, so i'm not entirely sure what you're complaining about.Janie
Yes, you can bind to strongly typed objects. But the binding itself isn't strongly typed at design- or compile-time. In other words, if the property "UsrID" doesn't exist, C# won't let me do this: string userID = myUser.UsrID; But if I want to do the same thing in XAML, (a) it doesn't give me any Intellisense hint that I'm doing it wrong; (b) it doesn't tell me when I compile my application that I'm doing it wrong; and (c) most astonishingly it doesn't even tell me that I'm doing it wrong when I run my application. It just fails silently. <TextBlock Text="{Binding UsrID}" />Ken Smith
I'd usually agree with you that lack of intellisense support in any part of the development experience within VS really throws you off. However, in this case; once you bind the datacontext of a particular piece of UI to a CLR object, it's really not hard to go back and validate that the names of the properties should be matching up. Your initial complaint seems like it stemmed from attempting to bind to the XML static resource however...Janie
It's not that your question needs to be more clear, its that you are missing that the lack of coupling between the view and the type you are binding to is actually a feature. What you are looking for is something sort of like ASP.NET MVC provides where you can have Page:ViewBase<T> where T is the type you intend to create the view for. This is ok, but is certainly limiting. You couldn't apply a view to two different types unless they shared a common point of inheritance. This moves away from WPF's goal of infinite composability. This is why you are getting the downvotes.Anderson Imes
Here's another way to put it. We're all used to losing Intellisense/strong typing when we have to interface from one technology cluster to another, e.g., when I make SQL calls from C#, or call JavaScript from Silverlight. And that's what XAML feels like to me. But it shouldn't. Yes, there should clearly be a separation of concerns between C# and XAML: but they're both designed to work within the common type system, so they should both use those same types, especially for data binding.Ken Smith

10 Answers

5
votes

Ken, C# would benefit from a concise syntactical element for referencing a PropertyInfo class. PropertyInfo structures are static objects defined at compile time and as such provide a unique key for every Property on an object. Properties could then be validated at compile time.

The only problem with this is the weirdness of treating an instance of a object as a data type given that strong typing is enforced on types, not the values of a type. Traditionally, compilers don't enforce data values, and instead rely on runtime code to check its data. Most cases its not even possible to validate data at compile time, but reflection is one of those edge cases where it is at least possible.

Alternatively, the compiler could create a new data type for every property. I could imagine a lot of types being created, but that would enable compile time enforcement of property binding.

One way to think about it is that the CLR introduced a level of reflection that was another level of magnitude compared to the systems that preceded it. It's now being used to do some rather impressive stuff like data binding. But its implementation is still at a metadata level, a kind of report from the compiler for every data type is generates. I supposed one way to grow C# would be to promote the metadata to compile time checking.

It seems to me that someone could develop a compilation tool that adds that reflection level validation. The new intellisense is sorta like that. It would be tricky to generically discover string parameters that are destined to be compared to PropertyInfos, but its not impossible. A new data type like "PropertyString" could be defined that clearly identifies parameters that will be compared to PropertyInfos in the future.

Anyway, I feel your pain. I've chased down a lot of misspelled property name references. Honestly, there are a lot of irritations in WPF related to reflection. A useful utility would be a WPF enforcement checker that makes sure all your static control constructors are in place, your attributes properly defined, bindings are accurate, correct keys, etc. There is a long list of validations that could be performed.

If I were still working for Microsoft, I'd probably try doing it.

10
votes

There will be IntelliSense support for data-binding in Visual Studio 2010. That seems to be what your complaint really boils down to, since data-binding is strongly-typed. You just don't find out until run-time whether or not the binding succeeded, and more often than not it fails quietly rather than with a noisy exception. When a binding fails, WPF dumps explanatory text via debug traces, which you can see in the Visual Studio output window.

Besides the lack of IntelliSense support and a few weird syntax issues, data-binding is quite well done (at least in my opinion). For some more assistance debugging data-bindings, I would check out Bea's lovely article here.

9
votes

This is my biggest gripe with XAML! Not having the compiler enforce valid databindings is a BIG issue. I don't really care about intellisense, but I DO care about the lack of refactoring support.

Changing property names or types is dangerous in a WPF application - using the built in refactoring support won't update data bindings in XAML. Doing a search and replace on the name is dangerous as it may change code that you didn't intend to change. Going through a list of find results is a pain in the arse and time consuming.

MVC has had strongly typed views for some time - the MVC contrib project provided them for MVC1 and MVC2 provides them natively. XAML must support this in future, particularly if it is used in "agile" projects where an application's design evolves over time. I haven't looked at .NET 4.0 / VS2010, but I hope the experience is far better than it is!

6
votes

I feel that xaml is sth like old-days html. i cannot imagine that after 10+ years, i am doing programming in that way: typing open-close tags manually because i cannot have a matured GUi for me to define styles and binding and templates. I strongly support your view, Ken. I feel very weird why so many people are supporting MVVM without a single complain about the pain in debugging xaml. Command binding and data binding are very good concepts and I have been designing my winform apps in that way. However, the xaml binding solution together with some other xaml issue (or lack of a sophisticated feature) is really a big failure in VS. It made the development and debug very difficult, and made the code very unreadable.

2
votes

It sounds like what you are asking for would not require any framework changes, and possibly is already fixed in VS 2010 (I don't have it installed).

The XAML designer needs IntelliSense for bindings within a DataTemplate when you specify a DataType, which is already possible like so:

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

I agree that having IntelliSense in this case would be a helpful change, but your other suggestions seem to miss the point of being able to change DataContexts at runtime and use DataTemplates for different types to render them uniquely.

2
votes

This is truly a solution to what you are wanting!

But, it isn't is a built-in, native, framework solution. Yes, I think that's what we all really want here. Maybe we'll get that later.

In the interim, if you are dead set on restricting the type, this solves that!

Using this converter:

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

Then, restrict your DataType like this:

<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>

See how I am requiring the CollectionViewSource to have System.Int16? Of course, you can't even set the Source of a CVS to an Integer, so this will always fail. But it proves the point for sure. It's just too bad that Silverlight does not support {x:Type} or I could have done something like ConverterParameter={x:Type sys:Int16} which would have been nice.

Plus, that XAML should be non-intrusive, so you should be able to implement this without any risk. If the data type is ever what you do not want it to be then the debugger will break and you can kick yourself for breaking your own rule. :)

Again, I know this is a little funky, but it does what you want - in fact I have been playing with it while coding it and maybe I even have a use for it. I like that it's design-time/debuggin only. But, listen, I am not trying it sell it to you.

I am just having fun, if this is too much syntax, just enjoy my effort ;)

PS: you could also create an attached property of type Type that you could use the same way. You could attach it to your CVS or anything else, like x:RequiredType="System.Int16" and in the behavior of the property you could just repeat the converter logic. Same effect, probably the same amount of code - but another viable option if you are serious.

1
votes

Okay I could not resist, here's the Attached Property approach:

Here's the 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>

Here's the property code:

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

Best of luck! Just having fun.

0
votes

Also, if you have your output window open when you're debugging your project, VS will inform you of any databinding errors, i.e. that the property a Control is bound to doesn't exist.

0
votes

Guys, What Ken and grant are trying to say is ..

How About have a XAMl where i can do like p.UserId)> where P is the DataContext of Type Customer