Search code examples
wpfxamlcontentcontrolcontentpresenter

WPF Binding with DataContext on Custom Content Control


I have a custom wizard control WizardControl deriving from UserControl which has a dependency property called Pages with a data type of my custom class collection called WizardPageCollection.

The WizardControl is hosted in a Window with a view model called MainViewModel and the pages of the wizard instantiated using XAML.

I am trying to bind the pages to sub-view models Page1VM and Page2VM declared as properties on the MainViewModel.

The first page binding of DataContext to Page1VM works fine, however the binding of the second page fails with the following error message:

System.Windows.Data Error: 3 : Cannot find element that provides DataContext. BindingExpression:Path=Page2VM; DataItem=null; target element is 'MyPage' (Name=''); target property is 'DataContext' (type 'Object')

Q. Why does the binding work on the first page but fail on the second and is there a way I can get this to work whilst still keeping the MainViewModel declared within the DataContext XAML tags of MainWindow? I would prefer not to use the ViewModel as a dictionary resources as this has some implications for us which I won't go into detail about.

As suggested by a commentor, if I change the binding to use RelativeSource as follows:

<common:MyPage DataContext="{Binding DataContext.Page1VM, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
<common:MyPage DataContext="{Binding DataContext.Page2VM, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

The first binding works ok, but the second one still fails, but with a different error message (as expected):

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=DataContext.Page2VM; DataItem=null; target element is 'MyPage' (Name=''); target property is 'DataContext' (type 'Object')

Thanks for your time!

My code listing is shown below:

MainWindow XAML:

<Window.DataContext>
    <common:MainViewModel />
</Window.DataContext>
<Grid>
    <common:WizardControl>
        <common:WizardControl.Pages>
            <common:WizardPageCollection>
                <common:MyPage DataContext="{Binding Page1VM}" />
                <common:MyPage DataContext="{Binding Page2VM}" />
            </common:WizardPageCollection>
        </common:WizardControl.Pages>
    </common:WizardControl>
</Grid>

MainViewModel and PageViewModel:

public class MainViewModel
{
    public PageViewModel Page1VM
    {
        get;
        set;
    }

    public PageViewModel Page2VM
    {
        get;
        set;
    }

    public MainViewModel()
    {
        this.Page1VM = new PageViewModel("Page 1");
        this.Page2VM = new PageViewModel("Page 2");
    }
}

public class PageViewModel
{
    public string Title { get; set; }
    public PageViewModel(string title) { this.Title = title; }
}

WizardControl XAML:

<Grid>
    <ContentPresenter Grid.Row="0" x:Name="contentPage"/>
</Grid>

WizardControl code-behind:

public partial class WizardControl : UserControl
{
    public WizardControl()
    {
        InitializeComponent();
    }

    public WizardPageCollection Pages
    {
        get { return (WizardPageCollection)GetValue(PagesProperty); }
        set { SetValue(PagesProperty, value); }
    }

    public static readonly DependencyProperty PagesProperty =
        DependencyProperty.Register("Pages", typeof(WizardPageCollection), typeof(WizardControl), new PropertyMetadata(new WizardPageCollection(), new PropertyChangedCallback(Pages_Changed)));

    static void Pages_Changed(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        WizardPageCollection col =  e.NewValue as WizardPageCollection;
        WizardControl ctrl = obj as WizardControl;
        ctrl.contentPage.Content = col.First();
    }
}

public class WizardPageCollection : ObservableCollection<WizardPageBase> { }

public class WizardPageBase : ContentControl { }

MyPage XAML:

<Grid>
    <Label Content="{Binding Title}"   />
</Grid>

Solution

  • Your approach depends on the value inheritance of the Window's DataContext property, which doesn't work with your WizardPageCollection because it doesn't form a WPF element tree.

    You should instead create your MainViewModel as a resource, and then reference it by StaticResource:

    <Window ...>
        <Window.Resources>
            <common:MainViewModel x:Key="MainViewModel"/>
        </Window.Resources>
        <Window.DataContext>
            <Binding Source="{StaticResource MainViewModel}"/>
        </Window.DataContext>
        <Grid>
            <common:WizardControl>
                <common:WizardControl.Pages>
                    <common:WizardPageCollection>
                        <common:MyPage DataContext="{Binding Page1VM,
                                           Source={StaticResource MainViewModel}}"/>
                        <common:MyPage DataContext="{Binding Page2VM,
                                           Source={StaticResource MainViewModel}}"/>
                    </common:WizardPageCollection>
                </common:WizardControl.Pages>
            </common:WizardControl>
        </Grid>
    </Window>