Tharaka Wijebandara
Tharaka Wijebandara

Reputation: 8065

Windows 10 Universal Compiled binding (x:Bind) conflict with ViewModel

I want to bind an element in a page to dependency property in code behind with compiled binding and same time bind another element to ViewModel with usual binding. But it gives a runtime error.

Here is my xaml code.

<Page
x:Class="XbindingProblem.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:XbindingProblem"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Main, Source={StaticResource Locator}}"
mc:Ignorable="d">
<Page.Resources>
    <DataTemplate x:Key="UserDataTemplate" x:DataType="local:User">
        <StackPanel>
            <TextBlock Text="{x:Bind Name}" />
            <TextBlock Text="{x:Bind Age}" />
        </StackPanel>
    </DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
        <TextBlock Text="{Binding Title}"/>
        <ContentPresenter ContentTemplate="{StaticResource UserDataTemplate}" Content="{x:Bind CurrentUser, Mode=OneWay}"/>
    </StackPanel>        
</Grid>

Here CurrentUser is dependency property which is initially null and then change in run time. This gives following runtime error.

Incorrect type passed into template. Based on the x:DataType global::XbindingProblem.User was expected.

The problem is it passes the ViewModel into UserDataTemplate instead of CurrentUser dependency property when CurrentUser is null.

Can anyone have a good explanation on this problem?

Upvotes: 3

Views: 2339

Answers (3)

Adam Kinney
Adam Kinney

Reputation: 1075

Although it breaks the idea of MVVM a bit, you can add a property to your page like this:

public MainViewModel viewModel => DataContext as MainViewModel;

And then in the XAML code reference the page property

<ContentPresenter Content="{x:Bind viewModel.CurrentUser, Mode=OneWay}" />

Upvotes: 1

Justin XL
Justin XL

Reputation: 39006

If you remove DataContext="{Binding Main, Source={StaticResource Locator}}", it will work. Why? because {x:Bind CurrentUser} is looking for a property called CurrentUser sitting inside your MainPage.xaml.cs. Since the CurrentUser is indeed a dependency property of your page, it will just work.

However, by specifying the DataContext of your page, the x:Bind is now excepting a CurrentUser property inside your MainViewModel instance, and of course it's not going to find it, so a compile-time error will be thrown.

One possible fix is to set the this.CurrentUser really early, even before calling InitializeComponent.

this.CurrentUser = new User();

InitializeComponent();

But this is IMHO not the the right way of doing things, as it's basically a racing game - it tries to populate the ContentPresenter before the DataContext gets updated, and in the end you will end up having the TextBlock (of which Text binds to Title) and the ContentPresenter attached to different contexts!

So ask yourself why you need to create a dependency property for CurrentUser inside a Page object, instead of having a normal property (with INotifyPropertyChanged implementation) sitting inside your MainViewModel? I'd prefer the latter, 'cause it's more semantically correct.

Upvotes: 5

Juan Pablo Garcia Coello
Juan Pablo Garcia Coello

Reputation: 3232

The question is interesting, what I have done is just remove the datacontext and this is the code behind is similar to yours:

public sealed partial class BlankPage1 : Page
{
    public User CurrentUser
    {
        get { return (User)GetValue(CurrentUserProperty); }
        set { SetValue(CurrentUserProperty, value); }
    }

    // Using a DependencyProperty as the backing store for CurrentUser.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CurrentUserProperty =
        DependencyProperty.Register("CurrentUser", typeof(User), typeof(BlankPage1), new PropertyMetadata(null));

    public BlankPage1()
    {
        this.InitializeComponent();

        CurrentUser = new User() { Name = "Hello", Age = "20" };
    }
}

public class User
{
    public String Name { get; set; }
    public String Age { get; set; }
}

might be you have the User class in another namespace or have another class in the typeof(...) of the dependency property. Because I tested that and works. The DataContext of the page can be whatever you want it won't affect.

Then I added the datacontext just to test:

<Page.DataContext>
    <local:Main/>
</Page.DataContext>

and the code just for testing:

public class Main
{
    public String Title { get; } = "title";
    public User MainUser { get; set; }
}

And it does not throws any exception, appears the Main data and the CurrentUser data.

UPDATE. The error happens when the User is null so it is like the x:Bind is null it propagates to the Binding,To solve that (it was tough):

<Page x:Name="Root"
    x:Class="Deletetb.BlankPage1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Deletetb"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" >
    <Page.DataContext>
        <local:Main/>
    </Page.DataContext>

    <Page.Resources>
        <DataTemplate x:Key="UserDataTemplate" x:DataType="local:User">
            <StackPanel>
                <TextBlock Text="{x:Bind Name}" />
                <TextBlock Text="{x:Bind Age}" />
            </StackPanel>
        </DataTemplate>
    </Page.Resources>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel DataContext="{x:Null}">
            <TextBlock Text="{Binding DataContext.Title, ElementName=Root}" />
            <ContentPresenter  ContentTemplate="{StaticResource UserDataTemplate}" Content="{x:Bind CurrentUser, Mode=OneWay}"/>
        </StackPanel>
    </Grid>
</Page>

Where is binding defined (TextBlock) I set the datacontext to null in the parent container (StackPanel) and bind by element name, and it does not crash, also I added a wait by code to test and set the Current User and it works. That was a challenge. hope it also works for you.

Upvotes: 1

Related Questions