Blackstar
Blackstar

Reputation: 161

How can I set DataTemplate to a ContentControl in a PageResource in Windows Universal App 10?

How can I set DataTemplate to a ContentControl in a PageResource? I would like to display a UserControl in my ContentControl and I want use the ContentControl like a navigation region. So it can change the UserControls what are displayed in it.

I have a Shell.xaml:

<Page
    x:Class="MyProject.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    
    xmlns:local="using:MyProject"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:View="using:MyProject.View"
    xmlns:ViewModel="using:MyProject.ViewModel">

    <Page.DataContext>
        <ViewModel:ShellViewModel />
    </Page.DataContext>

    <Page.Resources>
        <DataTemplate>
             <View:MyUserControlViewModel1 />
        </DataTemplate>

        <DataTemplate>
             <View:MyUserControlViewModel2 />
        </DataTemplate>
    </Page.Resources>

    <StackPanel>

        <ItemsControl ItemsSource="{Binding PageViewModels}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding Name}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

        <ContentControl Content="{Binding CurrentPageViewModel}">
        </ContentControl>
    </StackPanel>
</Page>

My Shell's view model is:

namespace MyProject.ShellViewModel
{
    class ShellViewModel : ObservableObject
    {
        #region Fields

        private ICommand _changePageCommand;
        private IPageViewModel _currentPageViewModel;
        private List<IPageViewModel> _pageViewModels;

        #endregion

        #region Properties / Commands

        public List<IPageViewModel> PageViewModels
        {
            get
            {
                if (_pageViewModels == null)
                {
                    _pageViewModels = new List<IPageViewModel>();
                }
                return _pageViewModels;
            }
        }

        public IPageViewModel CurrentPageViewModel
        {
            get { return _currentPageViewModel; }
            set
            {
                if (_currentPageViewModel != value)
                {
                    _currentPageViewModel = value;
                    OnPropertyChanged("CurrentPageViewModel");
                }
            }
        }
        #endregion

        #region Methods

        public ShellViewModel()
        {
            PageViewModels.Add(new MyUserControlViewModel1());
            PageViewModels.Add(new MyUserControlViewModel2());

            CurrentPageViewModel = PageViewModels[0];
        }

        #endregion
    }
}

I tried set Page.Resource like below from this link: Window vs Page vs UserControl for WPF navigation?

<Window.Resources>
      <DataTemplate DataType="{x:Type local:HomeViewModel}">
         <local:HomeView /> <!-- This is a UserControl -->
      </DataTemplate>
      <DataTemplate DataType="{x:Type local:ProductsViewModel}">
         <local:ProductsView /> <!-- This is a UserControl -->
      </DataTemplate>
   </Window.Resources>

But these use another namespaces and it doesn't work for me because my app is a Windows 10 Universal App and there is no DataType attribute for the DataTemplate for example.

I am trying to make my application using MVVM pattern (if it was not obiously from the code snippets).

Upvotes: 4

Views: 1469

Answers (1)

Martin Zikmund
Martin Zikmund

Reputation: 39072

You can do it by creating a class derived from DataTemplateSelector.

In this class you can override SelectTemplateCore method, that will return the DataTemplate you need based on the data type. To make all this more auto-magical, you can set the key of each template to match the name of the class and then search for the resource with that name retrieved using GetType().Name.

To be able to provide specific implementations on different levels, you can walk up the tree using VisualTreeHelper.GetParent() until the matching resource is found and use Application.Current.Resources[ typeName ] as fallback.

To use your custom template selector, just set it to ContentTemplateSelector property of the ContentControl.

Example

Here is the sample implementation of an AutoDataTemplateSelector

public class AutoDataTemplateSelector : DataTemplateSelector
{
    protected override DataTemplate SelectTemplateCore( object item ) => GetTemplateForItem( item, null );

    protected override DataTemplate SelectTemplateCore( object item, DependencyObject container ) => GetTemplateForItem( item, container );

    private DataTemplate GetTemplateForItem( object item, DependencyObject container )
    {
        if ( item != null )
        {
            var viewModelTypeName = item.GetType().Name;
            var dataTemplateInTree = FindResourceKeyUpTree( viewModelTypeName, container );
            //return or default to Application resource
            return dataTemplateInTree ?? ( DataTemplate )Application.Current.Resources[ viewModelTypeName ];
        }
        return null;
    }

    /// <summary>
    /// Tries to find the resources up the tree
    /// </summary>
    /// <param name="resourceKey">Key to find</param>
    /// <param name="container">Current container</param>
    /// <returns></returns>
    private DataTemplate FindResourceKeyUpTree( string resourceKey, DependencyObject container )
    {
        var frameworkElement = container as FrameworkElement;
        if ( frameworkElement != null )
        {
            if ( frameworkElement.Resources.ContainsKey( resourceKey ) )
            {
                return frameworkElement.Resources[ resourceKey ] as DataTemplate;
            }
            else
            {
                return FindResourceKeyUpTree( resourceKey, VisualTreeHelper.GetParent( frameworkElement ) );
            }
        }
        return null;
    }
}

You can now instantiate it as a resource and create resources for each type of ViewModel you use

<Application.Resources>
    <local:AutoDataTemplateSelector x:Key="AutoDataTemplateSelector" />

    <!-- sample viewmodel data templates -->
    <DataTemplate x:Key="RedViewModel">
        <Rectangle Width="100" Height="100" Fill="Red" />
    </DataTemplate>
    <DataTemplate x:Key="BlueViewModel">
        <Rectangle Width="100" Height="100" Fill="Blue" />
    </DataTemplate>
</Application.Resources>

And now you can use it with the ContentControl as follows:

<ContentControl ContentTemplateSelector="{StaticResource AutoDataTemplateSelector}"
   Content="{x:Bind CurrentViewModel, Mode=OneWay}" />

I have put the sample solution on GitHub

Upvotes: 4

Related Questions