views:

159

answers:

2

This started with weird behaviour that I thought was tied to my implementation of ToString(), and I asked this question: http://stackoverflow.com/questions/2916068/why-wont-wpf-databindings-show-text-when-tostring-has-a-collaborating-object

It turns out to have nothing to do with collaborators and is reproducible.

When I bind Label.Content to a property of the DataContext that is declared as an interface type, ToString() is called on the runtime object and the label displays the result.

When I bind TextBlock.Text to the same property, ToString() is never called and nothing is displayed. But, if I change the declared property to a concrete implementation of the interface, it works as expected.

Is this somehow by design? If so, any idea why?

To reproduce:

  • Create a new WPF Application (.NET 3.5 SP1)
  • Add the following classes:
public interface IFoo
{
    string foo_part1 { get; set; }
    string foo_part2 { get; set; }
}

public class Foo : IFoo
{
    public string foo_part1 { get; set; }

    public string foo_part2 { get; set; }

    public override string ToString() 
    { 
        return foo_part1 + " - " + foo_part2; 
    }
}
public class Bar
{
    public IFoo foo 
    { 
        get { return new Foo {foo_part1 = "first", foo_part2 = "second"}; } 
    }
}
  • Set the XAML of Window1 to:

    <Window x:Class="WpfApplication1.Window1"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         Title="Window1" Height="300" Width="300">
         <StackPanel>
            <Label Content="{Binding foo, Mode=Default}"/>
            <TextBlock Text="{Binding foo, Mode=Default}"/>
        </StackPanel>
    </Window>
    
  • in Window1.xaml.cs:

public partial class Window1 : Window  
{  
    public Window1()  
    {  
        InitializeComponent();  
        DataContext = new Bar();  
    }  
}

When you run this application, you'll see the text only once (at the top, in the label). If you change the type of foo property on Bar class to Foo (instead of IFoo) and run the application again, you'll see the text in both controls.

A: 

Well following on from my comment to your other question, you are using the fact that WPF will fall-back to ToString if you do not directly bind it to something e.g. a property on an object. Using the fall-back behaviour of ToString is not really the WPF way - expose a real object and bind to a real property and you'll be fine.

Update

Ok, I was reluctant to actually delve into the code as you are relying on a "fall-back" feature, it is a fall-back feature as this is what happens when you haven't specified a full binding - it is the last resort. I know you really want to use this feature, but you're really ignoring one of the greatest features of WPF - databinding. I can see from your C# naming that you have your own way of doing things, so maybe this isn't going to wash - no offense :)

The problem lies in the fact that the fall-back mechanism isn't consistent, as it is falling back to it in one usage and not in another. I'm really not sure why, and it's not worth knowing why - this is not how WPF is generally used.

If you use full binding you'll get what you want. A modified version of your example:

public interface IFoo
{
    string foo_part1 { get; set; }
    string foo_part2 { get; set; }
    string foo_all_parts { get; }
}

public class Foo : IFoo
{
    public string foo_part1 { get; set; }

    public string foo_part2 { get; set; }

    public string foo_all_parts
    {
       get
       {
           return foo_part1 + " - " + foo_part2;
       }
    }

    public override string ToString()
    {
        return foo_part1 + " - " + foo_part2;
    }
}

Xaml:

<Window x:Class="BindingToString.Window1"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="Window1" Height="300" Width="300">
 <StackPanel>
    <Label Content="{Binding foo, Mode=Default}"/>
    <TextBlock Text="{Binding foo.foo_all_parts, Mode=Default}"/>
  </StackPanel>
</Window>
chibacity
I hear what you are saying about relying on ToString(). I'm not sure why that is considered a "fall-back," though, or a non-feature. At this point, I'm trying to understand if there is an underlying *reason* for the inconsistency. As for IFoo not having a `ToString` member, declaring one doesn't make a difference (the runtime type is always going to implement it, by virtue of deriving from `object`). Also, it doesn't account for the fact that `Label.Content` can resolve it without a problem.
Jay
Granted about ToString, I'm tired and confused, thought it would be something to do with WPF reflecting on the type. I ran up some reflection code to bottom it out. Need sleep...
chibacity
+1  A: 

Yes you are right. Obviously the ContentControl.Content property is implemented differently from the TextBlock.Text property. One obvious difference of course is that the ContentControl will actually generate a TextBlock instance for a content object that is not a Visual and doesn't have a DataTemplate. The TextBlock does not. It will render the text by itself. In both cases the string is determined by

  1. the IValueConverter (if present in the binding)
  2. the TypeConverter (if declared)
  3. object.ToString()

It seems that this algorithm differs only in step 3 between TextBlock and ContentControl as you have shown. While the ContentControl actually resolves the object behind the interface, the TextBlock does not. Interesting.

I guess this is something that you have to live with. You have several options now:

  • Expose a string property on your interface and bind to that
  • Expose the data object as a concrete class instead as an interface
  • Use a ContentControl instead of a TextBlock
  • provide a IValueConverter and use it in the binding
  • provide a TypeConverter for your interface
  • do something else (there's probably more)
bitbonk