fsprojects / FsXaml

F# Tools for working with XAML Projects
http://fsprojects.github.io/FsXaml/
MIT License
171 stars 48 forks source link

UserControl data binding regression in 2.0.0 #38

Closed mbergin closed 8 years ago

mbergin commented 8 years ago

Description

Binding the DataContext of a UserControl using an ElementName binding worked in 0.9.9, does not work in 2.0.0

Repro steps

MainWindow.xaml

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp1">
    <StackPanel>
        <TextBox Name="Foo"/>
        <local:TestControl DataContext="{Binding ElementName=Foo, Path=Text}"/>
    </StackPanel>
</Window>

TestControl.xaml

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <TextBox Text="{Binding .}"/>
</UserControl>

Expected behavior

Typing into the TextBox Foo results in the same text in the TestControl.

Actual behavior

TestControl is blank. Binding error in Debug Output is

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=Foo'. BindingExpression:Path=Text; DataItem=null; target element is 'TestControl' (Name=''); target property is 'DataContext' (type 'Object')

Known workarounds

None.

Related information

ReedCopsey commented 8 years ago

@mbergin I've managed to duplicate this - and am not sure what's causing it to occur.

Note that it's not simply ElementName bindings that fail - most do work (I have quite a few in the samples), but rather ElementName (and potentially other?) bindings directly on loaded user controls.

For example, the 2nd text box works fine:

<TextBox x:Name="Foo" Grid.Row="3" Grid.Column="0" Text="Foo"/>
<local:UC Grid.Row="3" Grid.Column="1"  DataContext="{Binding ElementName=Foo, Path=Text}"/>
<TextBox Grid.Row="4" Grid.Column="0" Text="{Binding ElementName=Foo, Path=Text}"/>

Still investigating, and seeing what can be done. Will keep you posted if I figure something out.

ReedCopsey commented 8 years ago

@mbergin I'm not sure how to fix this right now - but I do have a workaround.

You can work around this by changing your XAML. Instead of writing:

<StackPanel>
    <TextBox Name="Foo"/>
    <local:TestControl DataContext="{Binding ElementName=Foo, Path=Text}"/>
</StackPanel>

You can use:

<StackPanel>
    <TextBox Name="Foo"/>
    <ContentPresenter Content="{Binding ElementName=Foo, Path=Text}">
      <ContentPresenter.ContentTemplate>
        <DataTemplate>
          <local:TestControl />
        </DataTemplate>
      </ContentPresenter.ContentTemplate>
    </ContentPresenter>
</StackPanel>

I know this isn't ideal - but at least it acts as a temporary workaround.

You can also move the template into a resource, in which case the usage becomes much shorter.

mbergin commented 8 years ago

Thanks for checking it out. Some googling revealed another workaround.

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp1">
    <StackPanel>
        <TextBox Name="Foo"/>
        <local:TestControl DataContext="{Binding Source={x:Reference Foo}, Path=Text}"/>
    </StackPanel>
</Window>

This is from ElementName Binding is failing -- some of the comments mention the logical tree might be disconnected, leading to Why does binding fail when binding a child element to another element when the parent succeeds?

Since you mentioned this in the release notes:

UserControl types no longer are wrapped within a ContentControl when loaded from XAML.

I'm not that familiar with the way the WPF logical tree works but I thought perhaps this could be related.

ReedCopsey commented 8 years ago

@mbergin Yes, I think there's a namescope issue -and I'm not 100% sure that there's a good workaround, either. I do think using x:Reference is a "good enough" workaround I may not worry about it too much, though.

ruxo commented 8 years ago

@ReedCopsey I found that System.Windows.Markup.IComponentConnector plays an important role here (but I don't know exactly how).

By implementing our UI class with this interface and the binding will work.

Example (sorry, haven't tried the new library yet):

open RZ.Wpf.CodeBehind

type TestControl() as me =
  inherit UserControl()

  let mutable content_loaded = false

  do (me :> IComponentConnector).InitializeComponent()

  interface IComponentConnector with
    member x.InitializeComponent() =
      if not content_loaded then
          me.InitializeCodeBehind "testcontrol.xaml"

      content_loaded <- true

    member x.Connect(connection_id, target) = content_loaded <- true

(In case you might wonder, this I copied from C# WPF generated code :D )

ReedCopsey commented 8 years ago

@ruxo Thanks - This definitely seems to correct it. I'm not sure why, since it's not doing anything - there must be code that uses IComponentConnector normally and just skips important steps if it doesn't exist.

I'm implementing this for FsXaml 2.1, in progress now. I'm actually doing the code slightly differently than you did above (so it's a bit more inline with what C# gives you), but it appears to be working correctly. Thanks for tracking this down!

@mbergin : This will be corrected in FsXaml 2.1. I hope to release shortly, but the generated type is changing significantly, so I'm trying to do some thorough testing.

ReedCopsey commented 8 years ago

This has now been solved in 2.1. @mbergin please try https://www.nuget.org/packages/FsXaml.Wpf/2.1.0

It should now work with the ElementName bindings (as well as your x:Reference workaround).

mbergin commented 8 years ago

Perfect, this works for us now without the workaround. Thanks @ReedCopsey @ruxo for fixing it so quickly!