Dynamically assign a UserControl

Topics: Questions
Jun 19, 2012 at 2:57 PM

Hi Everyone,

I am just starting with Catel framework and WPF. 

I am re-writting an WPF application I wrote a year ago.

Basically I have a property in my MainWindowViewModel that hold the current viewmodel the user is trying to deal with.

I would like to know what should I use in the MainWindow.xaml so the usercontrol associated with the current viewmodel can be displayed.

In my old application I am using a ContentControl and define some DataTemplates:

 <Window.Resources>
        <DataTemplate DataType="{x:Type vm:HomeViewModel}">
            <views:HomeView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:DetailReportViewModel}">
            <views:DetailReportView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:TransferViewModel}">
            <views:TransferView/>
        </DataTemplate>
        
    </Window.Resources>
    <Grid>
        <ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}"></ContentControl>
    </Grid>

I was hoping in Catel the locator would use the correct view associated with the viewmodel without specifying a Datatemplate.

Here is what i tried:

MainWindow.xaml:

<catel:DataWindow x:Class="GenericFileTransfer2.Views.MainWindow"
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
				  xmlns:catel="http://catel.codeplex.com"
                  ShowInTaskbar="True" ResizeMode="CanResize" SizeToContent="WidthAndHeight" WindowStartupLocation="Manual" WindowState="Normal" 
                  Height="600"
                  Width="600">
    
    <!-- Resources -->
    <catel:DataWindow.Resources>
    </catel:DataWindow.Resources>
    
    <!-- Content -->
     <catel:StackGrid x:Name="LayoutRoot">
		<catel:StackGrid.RowDefinitions>
			<RowDefinition Height="Auto" />
			<RowDefinition Height="Auto" />
		</catel:StackGrid.RowDefinitions>

        <Menu Height="23" HorizontalAlignment="Stretch" Name="menu1" VerticalAlignment="Top" Grid.Row="0">
            <MenuItem Header="_Actions">
                <MenuItem Header="Home" Command="{Binding HomeViewCommand}"/>
                <MenuItem Header="New" Command="{Binding AddReport}"/>
                <MenuItem Header="Edit" Command="{Binding EditReportViewCommand}"/>
                <MenuItem Header="Transfer" Command="{Binding TransferViewCommand}"/>
            </MenuItem>
        </Menu>
        <catel:UserControl Grid.Row="1" DataContext="{Binding CurrentViewModel}" />
    </catel:StackGrid>
</catel:DataWindow>

MainWindowViewModel.cs:

namespace GenericFileTransfer2.ViewModels
{
    using Catel.MVVM;
    using Catel.Data;

    /// <summary>
    /// MainWindow view model.
    /// </summary>
    public class MainWindowViewModel : ViewModelBase
    {
        #region Variables
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
        /// </summary>
        public MainWindowViewModel()
            : base()
        {
            HomeViewCommand = new Command(OnHomeViewCommandExecute, OnHomeViewCommandCanExecute);
            AddReport = new Command(OnAddReportExecute, OnAddReportCanExecute);
            CurrentViewModel = new HomeViewModel();
        }
        #endregion

        #region Properties

        public ViewModelBase CurrentViewModel
        {
            get { return GetValue<ViewModelBase>(CurrentViewModelProperty); }
            set { SetValue(CurrentViewModelProperty, value); }
        }

        /// <summary>
        /// Register the CurrentViewModel property so it is known in the class.
        /// </summary>
        public static readonly PropertyData CurrentViewModelProperty = RegisterProperty("CurrentViewModel", typeof(ViewModelBase), null);
        
        #endregion

        #region Commands
        // TODO: Register commands with the vmcommand or vmcommandwithcanexecute codesnippets
        /// <summary>
        /// Gets the name command.
        /// </summary>
        /// <summary>
        /// Gets the name command.
        /// </summary>
        public Command HomeViewCommand { get; private set; }

        /// <summary>
        /// Method to check whether the name command can be executed.
        /// </summary>
        /// <returns><c>true</c> if the command can be executed; otherwise <c>false</c></returns>
        private bool OnHomeViewCommandCanExecute()
        {
            return !CurrentViewModel.GetType().Equals(typeof(HomeViewModel));
        }

        /// <summary>
        /// Method to invoke when the name command is executed.
        /// </summary>
        private void OnHomeViewCommandExecute()
        {
            // TODO: Handle command logic here
            CurrentViewModel = new HomeViewModel();
        }


        /// <summary>
        /// Gets the name command.
        /// </summary>
        public Command AddReport { get; private set; }


        /// <summary>
        /// Method to check whether the name command can be executed.
        /// </summary>
        /// <returns><c>true</c> if the command can be executed; otherwise <c>false</c></returns>
        private bool OnAddReportCanExecute()
        {
            return !CurrentViewModel.GetType().Equals(typeof(ReportViewModel));
        }

        /// <summary>
        /// Method to invoke when the name command is executed.
        /// </summary>
        private void OnAddReportExecute()
        {
            CurrentViewModel = new ReportViewModel();
        }

        #endregion

        #region Methods
        // TODO: Create your methods here
        #endregion
    }
}

Do I have to use a ContentControl and specify DataTemplates or I am missing something?

Please let me know if you need more informations.

 

Thank you

 

Coordinator
Jun 20, 2012 at 4:03 PM

Unfortunately, what you are trying to accomplish is not possible. Catel will try to retrieve the view model belonging to a view (in this case the UserControl) based on naming convention (or you have to register it manually per type). It is not possible to use the same user control for multiple views.

One option you have is to define a user control (custom implementation) that has a shared base view model as it's own view model. Then, you can also register your own implementation of the IViewModelFactory and control exactly what view models are used based on the injected data context (in your case the view model). Then you can really do some crazy stuff, but this requires a good knowledge of what you are doing.

Jun 20, 2012 at 4:49 PM

Thank you for your reply.

Being new to WPF and MVVM i think I will get back to the "ContentControl" way of doing it.

 

Regis

Coordinator
Jun 20, 2012 at 4:51 PM

That is a smart decision. Once you are familiar with WPF, MVVM and Catel, you can try again and you will understand it all. But for now it's better not to do such complex stuff :)

Jul 11, 2013 at 6:30 PM
I wish Catel can solve this problem as Caliburn.Micro does.
In caliburn.micro, the view will automatically created depending on the type name of datacontext, without any DataTemplate or DataTemplateSelector.

e.g.
<ContentControl Content={Binding AAA}>

suppose in ViewModel, we have property
object AAA{ set.., get..., };

when we set
AAA = new ABCViewModel();
the ABCView will be created to fill the ContentControl,

and then when we change it
AAA = new CCCViewModel();
the CCCView will be created to fill the ContentControl
All based on the name convension.

This is really cool because you dont have to set datatemplate for each possible view. Which means when you want to change the view in ContentControl to a new one, you do not need to change XAML each time a new view is added. only one place of code need to be changed.
I believe Caliburn.Micro way solves this problem perfectly well.

Catel also uses name convention to resolve view, I wonder why not Catel does the same thing.
Catel already uses name convension to resolve view in the UIVisualizationService, but UIVisualizationService shows a whole window, it does not change part of the view in the window.

I really hope that Catel can extend a bit and do what Caliburn.Micro can do, or at least provide some new control, like "ViewModelHolder" that works like:
<ViewModelHolder Content={Binding AAA}>

and a "ViewModelCollectionHolder" that works like:
<ViewModelHolder Content={Binding AAAs}>
(note that each AAA can be of different viewmodel type, and their respective views shall be displayed)

So that I can totally abandon Caliburn.Micro and shift to Catel in my future projects.

Thank you very much
Coordinator
Jul 11, 2013 at 7:20 PM
I think the ViewModelToViewConverter does this what you want?

See https://catel.codeplex.com/discussions/393511