user978139
user978139

Reputation: 579

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>

Upvotes: 2

Views: 3395

Answers (2)

Liero
Liero

Reputation: 27360

@Clemens answer workarounds the problem but the problem is something else, imho.

  1. When item is added to WizardPageCollection, it should be added to LogicalTree as well. Look at sources of ItemsControl for inspiration. It is definitelly possible to make your binding works as they were.

  2. I would use viewmodel first approach here. Define pages as collection of page viewmodels and genereate the views. At the end the xaml would look like this:

    <common:WizardControl PagesSource="{Binding Pages}">
        <common:WizardControl.PageTemplate>
            <DataTemplate>
                <common:MyPage DataContext="{Binding }" />
            </DataTemplate>
        </common:WizardControl.PageTemplate>
    </common:WizardControl>
    

alternativelly, consider your WizardControl derive from Selector class instead usercontrol. (Selector is base class from listbox. It has itemssource and selected item).

    <common:WizardControl ItemsSource="{Binding Pages}" 
                          SelectedItem="{Binding SelectedPage}">
        <common:WizardControl.ItemTemplate>
            <DataTemplate>
                <common:MyPage DataContext="{Binding }" />
            </DataTemplate>
        </common:WizardControl.ItemTemplate>
    </common:WizardControl>

Upvotes: 0

Clemens
Clemens

Reputation: 128146

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>

Upvotes: 2

Related Questions