Dynamic Tab layout with Catel

Sep 5, 2011 at 8:52 AM

Hello,

 

looking at Catel it looks great, really great work but I'm just wondering is there a way to operate a dynamic tab layout with Catel? Is (or can) the UIVisualizer that brilliant to help with tabItems ?

 

Coordinator
Sep 5, 2011 at 8:56 AM

Thanks for your kind words :)

If you want to let Catel show views inside a tab, simply create a new UIVisualzierService (deriving from the existing one), and create a new tab with the user control as content. The only method you need to override is Show(string, object, eventhandler) which contains the logic to show the view in a non-modal window. The ShowDialog can still be used to show modal dialogs in that case.

Another option is the completely customize the UIVisualizerService by implementing the IUIVisualizerService yourself (but I wouldn't recommend that for such an easy task).

Good luck!

Sep 5, 2011 at 2:58 PM

Thank you for the reply and yes I see that it's going to work fine but... I think I would prefer to do Show(IViewModel) as it is plain easy. Could I then still use the UIVIsualizer to "find" the appropriate UserControl to the corresponding viewmodel just like it does with DataWindows? I can always maintain a big switch-case but I think that is just throwing away a powerful part of Catel.

Any help appreciated, thank you.

Coordinator
Sep 6, 2011 at 6:39 AM

That is still possible. If you use the latest source code (which will be 2.1), the UIVisualizerService supports multiple naming conventions (see documentation). This way, you can still use the Show<MyViewModel> and it will show inside a tab instead of a non-modal window.

No need for big switch-cases :)

Sep 6, 2011 at 9:25 AM

Thanks! Works great and I have to say that in the end I went with your original advice to override Show(name, data, proc).

 

Unfortunately I've encountered another little problem. Basically I wanted to remove the tab holding the view (Catels' UserControl inside a TabItem) when the Closed event fires on the ViewModel ( or do Close() on the viewmodel if the tab gets closed from the UI) but when I try to display a 2nd tab the previous tab gets closed because the Closed event fires on the viewModel. I've looked through the source code but I'm coming up short.

Don't know if this is a expected behaviour but every time the selected tab changes a PropertyChanged gets fired with PropertyName of "ParentViewModel". I'm creating a new viewModel for each view so not sure where this is coming from.

I can probably get around it one way or the other but again why throw away something which is there already.

Any help greatly appreciated.

Coordinator
Sep 6, 2011 at 10:03 AM

This is expected behavior, but as in everything with Catel, there is already a solution for this. For such situations, we invented the CloseViewModelOnUnloaded property. You can set it to false for user controls that will be used inside tabs. Actually, I would create a tab control which sets this for the content (so the user control can be re-used outside the tab control). 

This way, the UserControl gets unloaded (default tab control behavior), but Catel doesn't close the view model. The next time the control is loaded again, it re-uses the old view model. Keep in mind though that you will have to close the view model manually when closing the tab with a call to ViewModel.CloseViewModel(false) (or, if you are using the latest source, SaveAndCloseViewModel()).

Sep 6, 2011 at 12:23 PM

Again many thanks works like a charm.

 

Just wondering really - I tried calling the methods which you suggested to close the ViewModel but any of those calls comes back with InvalidOperationException:

System.InvalidOperationException was unhandled
  Message=DialogResult can be set only after Window is created and shown as dialog.
  Source=PresentationFramework
  StackTrace:
       at System.Windows.Window.set_DialogResult(Nullable`1 value)
       at Catel.Windows.Controls.MVVMProviders.Logic.WindowLogic.OnViewModelClosed(Object sender, ViewModelClosedEventArgs e)
       at Catel.MVVM.ViewModelBaseWithoutServices.CloseViewModel(Nullable`1 result)
       at Catel.MVVM.ViewModelBaseWithoutServices.SaveAndCloseViewModel()

  InnerException:

Looks like it's trying to deal with it like it would be a DataWindow not a UserControl ?

I've worked around it by setting he CloseViewModelOnUnloaded back to True and closing the tab. Strange thing is it doesn't happen when calling the method from the viewmodel itself.

Coordinator
Sep 6, 2011 at 12:25 PM

Yes, somehow it has instantiated a Window instead of a Control. Are you using WPF or SL?

Coordinator
Sep 6, 2011 at 12:38 PM

You did register the custom UIVisualizerService in the IoC container, right? And can you show me the Show implementation?

Sep 6, 2011 at 12:43 PM
Edited Sep 6, 2011 at 12:44 PM

I hope I did! I'm using WPF.

In the App.xaml.cs:

 

ServiceLocator.Instance.RegisterType<IUIVisualizerService, TabUIVisualizerService>(true);

 

And its implementation (I've copied / pasted the CreateWindow method and mostly renamed all Window types to Control)

 

public class TabUIVisualizerService : UIVisualizerService
    {
        public TabUIVisualizerService()
            : base()
        {
            RegisterTypesAutomatically(typeof(Catel.Windows.Controls.UserControl<>));
        }

        public override bool Show(string name, object data, EventHandler<UICompletedEventArgs> completedProc)
        {
            var tab = ServiceLocator.Instance.ResolveType<ITabDisplay>().MainTabControl;
            if (tab != null)
            {
                var control = CreateControl(name, data, completedProc);
                if (control != null)
                {
                    TabItemPB tabItem = new TabItemPB(control);
                    if (tabItem != null)
                    {
                        tab.Items.Add(tabItem);
                        tab.SelectedItem = tabItem;

                        (data as IViewModel).Closed += (x, y) =>
                        {
                            if (tab.Items.Contains(tabItem))
                                tab.Items.Remove(tabItem);
                            tabItem = null;
                        };

                        return true;

                    }
                    else
                        return false;
                }
                else
                    return false;
            }
            else
                return false;
        }

        private UserControl CreateControl(string name, object data, EventHandler<UICompletedEventArgs> completedProc)
        {
            Type controlType;
            lock (_registeredWindows)
            {
                if (!_registeredWindows.TryGetValue(name, out controlType))
                {
                    return null;
                }
            }

            UserControl control = null;

            // First, try to constructor directly with the data context
            if (data != null)
            {
                ConstructorInfo constructorInfo = controlType.GetConstructor(new[] { data.GetType() });
                if (constructorInfo != null)
                {
                    control = constructorInfo.Invoke(new object[] { data }) as UserControl;
                }
            }

            // Check if there is a window constructed
            if (control == null)
            {
                ConstructorInfo constructorInfo = controlType.GetConstructor(Type.EmptyTypes);
                if (constructorInfo == null)
                {
                    //Log.Error(TraceMessages.NoInjectionOrDefaultConstructorFoundForWindow, windowType);
                    return null;
                }

                control = constructorInfo.Invoke(new object[] { }) as UserControl;

                if (control != null)
                {
                    control.DataContext = data;
                }
            }

            if ((control != null) && (completedProc != null))
            {
                control.Unloaded += (s, e) => completedProc(this, new UICompletedEventArgs(data, null));
            }

            return control;
        }

    }
Coordinator
Sep 6, 2011 at 12:59 PM

The views are found in the _registeredWindows, and the views do derive from UserControl? Can you show me the code-behind of 1 of the views you are trying to show as a tab?

Sep 6, 2011 at 2:54 PM
Edited Sep 6, 2011 at 3:01 PM

Double checked the _registeredWindows and yes all of the UserControls are there including the MainWindow. The CreateControl method creates the right UserControl fine and sets the ViewModel right.

 

/Views/ProductListControl.xaml

Xaml:

 

<catel:UserControl x:Class="ProjBaby.Views.ProductListControl"
                   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		   xmlns:catel="http://catel.codeplex.com"
                   xmlns:ViewModels="clr-namespace:ProjBaby.ViewModels"
                   x:TypeArguments="ViewModels:ProductListViewModel">
    
    <!-- Resources -->
    <UserControl.Resources>

    </UserControl.Resources>

    <!-- Content -->
    <Grid>
        <Label Content="Here goes your real content" />
        <Button Content="Close" Command="{Binding Path=CloseCmd}" />
    </Grid>
</catel:UserControl>

 

code-behind:

namespace ProjBaby.Views
{
    using Catel.Windows.Controls;
    using ViewModels;

    /// 
    /// Interaction logic for ProductListControl.xaml.
    /// 
    public partial class ProductListControl : UserControl<ProductListViewModel>
    {
        /// 
        /// Initializes a new instance of the  class.
        /// 
        public ProductListControl()
        {
            InitializeComponent();
        }
    }
}

 

I haven't really touched the UserControl yet as I'm first trying the "how would it all work together" so it's all done by Catel templates.

Coordinator
Sep 6, 2011 at 3:14 PM

Can you send me the example, or is there too much "personal" work involved? I think it's the main window that also receives the CloseViewModel command. Therefore, are you sure you are calling it on the right view model (TabView.Content.ViewModel)? If you want to send the source, please contact me via the codeplex connect page and I will provide you with my e-mail.

Coordinator
Sep 6, 2011 at 7:41 PM

The problem is the custom tab control implementation. In the close button, you handle the close event like this:

 

var vm = this.DataContext as ViewModelBase;
if (vm != null)
{
    vm.SaveAndCloseViewModel();
}

 

However, this is the data context:


Window (WindowViewModel)

    |- Tab (WindowViewModel)       

        |- Content (UserControlViewModel)

So, in the code you are using the WindowViewModel to close the tab view model, and that is where it goes wrong. So, instead of the code below, use this code:

var tabContent = Content as FrameworkElement;
if (tabContent == null)
{
    return;
}

var vm = tabContent.DataContext as ViewModelBase;
if (vm != null)
{
    vm.SaveAndCloseViewModel();
}