I am building a cockpit, i have lot of user control (different types of switch) and i try to integrate them in a window

I am using caliburn and Ninject and try to keep MVVM.

So i have a problem, i have to integrate the different switch dynamically into the Grid of window and i dont know if i could keep MVVM

So in my solution i am using the name of the grid to place the different UserControls at different positions and i break MVVM

How i could do that with MVVM? I have read i could use ContentControl to bind the different ViewModels from a list, but i dont see how to do that.. some help will be welcome

Bootstrapper.cs:

using Caliburn.Micro;
using Ninject;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using TestNinjectCaliburn.ViewModels;
using EventAggregator = TestNinjectCaliburn.Events.EventAggregator;
using IEventAggregator = TestNinjectCaliburn.Events.IEventAggregator;

namespace TestNinjectCaliburn
{
    public class Bootstrapper : BootstrapperBase
    {
        private IKernel kernel;
        public Bootstrapper()
        {
            Initialize();
        }

        protected override void Configure()
        {
            kernel = new StandardKernel();

            kernel.Bind<IEventAggregator>().To<EventAggregator>().InSingletonScope();
            kernel.Bind<IWindowManager>().To<WindowManager>().InSingletonScope();
            kernel.Bind<MainWindowViewModel>().ToSelf().InSingletonScope();

            MessageBinder.SpecialValues.Add("$pressedkey", (context) =>
            {
                // NOTE: IMPORTANT - you MUST add the dictionary key as lowercase as CM
                // does a ToLower on the param string you add in the action message, in fact ideally
                // all your param messages should be lowercase just in case. I don't really like this
                // behaviour but that's how it is!
                var keyArgs = context.EventArgs as KeyEventArgs;

                if (keyArgs != null)
                    return keyArgs.Key;

                return null;
            });
        }

        protected override void OnStartup(object sender, StartupEventArgs e)
        {
            DisplayRootViewFor<MainWindowViewModel>();
        }
        protected override object GetInstance(Type service, string key)
        {
            return kernel.Get(service);
        }

        protected override IEnumerable<object> GetAllInstances(Type service)
        {
            return kernel.GetAll(service);
        }
    }
}

MainWindowViewModel.cs which calls SecondViewModel.cs

using Caliburn.Micro;
using IEventAggregator = TestNinjectCaliburn.Events.IEventAggregator;
using Ninject.Syntax;

namespace TestNinjectCaliburn.ViewModels
{
    public class MainWindowViewModel
    {
        private readonly IWindowManager windowmanager;
        private readonly SecondViewModel[] secondviewmodel;
        private readonly IEventAggregator eventAggregator;

        public MainWindowViewModel(IWindowManager windowmanager, IEventAggregator eventAggregator, IResolutionRoot resolutionRoot, SecondViewModel secondviewmodel)
        {
            this.eventAggregator = eventAggregator;
            this.windowmanager = windowmanager;
            this.secondviewmodel = new SecondViewModel[1];
            this.secondviewmodel[0] = new SecondViewModel(eventAggregator, resolutionRoot);
        }

        public void Launch()
        {
            windowmanager.ShowWindow(secondviewmodel[0]);
        }

    }
}

SecondViewModel.cs:

using Caliburn.Micro;
using Ninject;
using Ninject.Parameters;
using Ninject.Syntax;
using System;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using TestNinjectCaliburn.Views;
using IEventAggregator = TestNinjectCaliburn.Events.IEventAggregator;

namespace TestNinjectCaliburn.ViewModels
{
    public class SecondViewModel:Screen
    {
        private readonly IEventAggregator eventAggregator;
        private readonly IResolutionRoot resolutionRoot;
        private UserControl usercontrol;

        public SecondView secondView;
        public SecondViewModel(IEventAggregator eventAggregator, IResolutionRoot resolutionRoot)
        {
            this.eventAggregator = eventAggregator;
            this.resolutionRoot = resolutionRoot;
        }


        protected override void OnViewReady(object view)
        {
            secondView = view as SecondView;
        }

        public Type[] Typelist;
        //here i break MVVM ************************
        protected override void OnViewAttached(object secondview, object context)
        {
            Element[] elts = {
                new Element()
                {
                    viewmodel = Type.GetType("TestNinjectCaliburn.Gauges." + "SwitchOffOn_ViewModel"),
                    Top = 100,
                    Left = 100
                },
                new Element()
                {
                    viewmodel = Type.GetType("TestNinjectCaliburn.Gauges." + "SwitchOffOn_ViewModel"),
                    Top = 100,
                    Left = 200
                },
                new Element()
                {
                    viewmodel = Type.GetType("TestNinjectCaliburn.Gauges." + "SwitchOffOn_ViewModel"),
                    Top = 100,
                    Left = 300
                },
                new Element()
                {
                    viewmodel = Type.GetType("TestNinjectCaliburn.Gauges." + "SwitchOffOn_ViewModel"),
                    Top = 100,
                    Left = 400
                },
            };

            var MainGrid = (secondview as SecondView).MainGrid;

            for (int i = 0; i < elts.Length; i++)
            {
                Ninject.Parameters.Parameter[] param = {
                        new ConstructorArgument("left", elts[i].Left , true),
                        new ConstructorArgument("top", elts[i].Top, true)
                };

                // Replace the Activator.CreateInstance     
                var viewmodel = resolutionRoot.TryGet(elts[i].viewmodel, param);
                var view = ViewLocator.LocateForModel(viewmodel, null, null);

                ViewModelBinder.Bind(viewmodel, view, null);
                MainGrid.Children.Add(view);
            }

        }
    }

    public class Element
    {
        public Type viewmodel;
        public double Top;
        public double Left;
    }
}

An example of one of usercontrols i want to set:

SwitchOffOn_ViewModel.cs

using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using IEventAggregator = TestNinjectCaliburn.Events.IEventAggregator;

namespace TestNinjectCaliburn.Gauges
{
    public class SwitchOffOn_ViewModel : TemplateSwitch
    {
        private readonly IEventAggregator eventAggregator;

        public SwitchOffOn_ViewModel(IEventAggregator eventAggregator, double left, double top)
        {
            this.eventAggregator = eventAggregator;
            this.eventAggregator.Subscribe(this);
            //Tag = tag;

            var folder = Environment.CurrentDirectory + "\\Images\\Elements\\";
            SwitchImage = new string[] { folder + "switch_n0.png", folder + "switch_n2.png" };
            NbImages = SwitchImage.Length;
            SwitchIndex = 0;
            //double left = 200, top = 0;

            UCLeft = left;
            UCTop = top;

            InitialSize = 40;
            scaleX = InitialSize / (new BitmapImage(new Uri(SwitchImage[0])).PixelWidth / 2d);
            angle = 0d;

        }

        #region Mouse Events
        public void MouseEnter(MouseEventArgs e)
        {
            ToolTip = (e.OriginalSource as UserControl).Margin.ToString();
        }
        #endregion
    }
}

SwitchOffOn_View.xaml:

<UserControl 
    x:Class="TestNinjectCaliburn.Gauges.SwitchOffOn_View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:cal="http://www.caliburnproject.org"
    mc:Ignorable="d" d:DesignHeight="150" d:DesignWidth="70"
    x:Name="usercontrol" Height="150" Width="70" RenderTransformOrigin="0.5,0.5" Tag="{Binding Tag, Mode=OneTime}" ToolTip="{Binding ToolTip}"
    VerticalAlignment = "Top" HorizontalAlignment = "Left" ClipToBounds="True"
    cal:Message.Attach="[Event MouseEnter] = [Action MouseEnter($eventArgs)]">

    <UserControl.Margin>
        <MultiBinding Converter="{StaticResource MyMultiConverterMargin}">
            <Binding Path="UCLeft" UpdateSourceTrigger="PropertyChanged"></Binding>
            <Binding Path="UCTop" UpdateSourceTrigger="PropertyChanged"></Binding>
        </MultiBinding>
    </UserControl.Margin>

    <UserControl.LayoutTransform>
        <TransformGroup>
            <RotateTransform x:Name="rotation" Angle="{Binding angle}"/>
            <ScaleTransform x:Name="scale" ScaleX="{Binding scaleX}" ScaleY="{Binding ElementName=scale, Path=ScaleX}"/>
        </TransformGroup>
    </UserControl.LayoutTransform>

    <Grid RenderTransformOrigin="0.5,0.5">

        <Image x:Name="SwitchUp" Source="{Binding SwitchImage[1], Mode=OneTime}"
               Width="{Binding ElementName=usercontrol, Path=Width}"
               Height="{Binding ElementName=usercontrol, Path=Height}"
               HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Image.Style>
                <Style TargetType="{x:Type Image}">
                    <Setter Property="Visibility" Value="Hidden" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SwitchIndex}" Value="1">
                            <Setter Property="Visibility" Value="Visible" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>

        <Image x:Name="SwitchDown" Source="{Binding SwitchImage[0], Mode=OneTime}"
               Width="{Binding ElementName=usercontrol, Path=Width}"
               Height="{Binding ElementName=usercontrol, Path=Height}"
               HorizontalAlignment="Center" VerticalAlignment="Center" >
            <Image.Style>
                <Style TargetType="{x:Type Image}">
                    <Setter Property="Visibility" Value="Hidden" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SwitchIndex}" Value="0">
                            <Setter Property="Visibility" Value="Visible" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>

        <Rectangle x:Name="UpperRec" Visibility="Visible" Margin="5,5,0,0"
                   cal:Message.Attach="[Event MouseLeftButtonDown] = [Action MouseLeftButtonDown('true')]"
                   Width="{Binding ElementName=usercontrol, Path=Width, Converter={StaticResource MyConverterSize}, ConverterParameter=1 10}"
                   Height="{Binding ElementName=usercontrol, Path=Height, Converter={StaticResource MyConverterSize}, ConverterParameter=2 10}"
                   HorizontalAlignment="Left" VerticalAlignment="Top" Style="{StaticResource IsModeEdit}" >
        </Rectangle>

        <Rectangle x:Name="LowerRec" Visibility="Visible" Margin="0 0 5 5"
                   cal:Message.Attach="[Event MouseLeftButtonDown] = [Action MouseLeftButtonDown('false')]"
                   Width="{Binding ElementName=UpperRec, Path=Width}"
                   Height="{Binding ElementName=UpperRec, Path=Height}"
                   HorizontalAlignment="Right" VerticalAlignment="Bottom" Style="{StaticResource IsModeEdit}" >
        </Rectangle>

        <Rectangle x:Name="DesignFrame"
                   Visibility="{Binding Frame, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource BoolToVisConverter}}"
                   Width="{Binding ElementName=usercontrol, Path=Width, UpdateSourceTrigger=PropertyChanged}"
                   Height="{Binding ElementName=usercontrol, Path=Height, UpdateSourceTrigger=PropertyChanged}"
                   Margin="0,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Style="{StaticResource IsModeSelected}" >
        </Rectangle>

    </Grid>
</UserControl>

MainWindowView.xaml:

<Window x:Class="TestNinjectCaliburn.Views.MainWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindowView" Height="250" Width="400">
    <Grid>
        <Button x:Name = "Launch" Content ="Launch Test Ninject and Caliburn" HorizontalAlignment="Left" Margin="115,25,0,0" VerticalAlignment="Top" Width="209"/>
    </Grid>
</Window>

SecondView.xaml:

<Window x:Class="TestNinjectCaliburn.Views.SecondView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestNinjectCaliburn.Views"
        mc:Ignorable="d"
        Title="SecondView" Height="400" Width="800">
    <Grid x:Name="MainGrid">

    </Grid>
</Window>

the result i would have (keeping MVVM):

enter image description here


Following your answer,

i have created BaseViewModel for all my usercontrols

namespace TestNinjectCaliburn.Gauges
{
    public abstract class BaseViewModel:TemplateSwitch
    {
    }
}

i have added the BaseViewModel to my ViewModels

public class SwitchOn_Off_On_ViewModel : BaseViewModel

public class SwitchOffOn_ViewModel : BaseViewModel

i have added in SecondViewModel.cs:

the definition of the collection of ViewModel:

    private ObservableCollection<BaseViewModel> _myCockpitViewModels = new ObservableCollection<BaseViewModel>();
    public ObservableCollection<BaseViewModel> MyCockpitViewModels
    {
        get { return _myCockpitViewModels; }
        set
        {
            _myCockpitViewModels = value;
            NotifyOfPropertyChange(() => MyCockpitViewModels);
        }
    }

and i have loaded the list:

        for (int i = 0; i < elts.Length; i++)
        {
            //if (!typelist[i].ToString().Replace("_", "").Contains("Switch")) continue;
            Ninject.Parameters.Parameter[] param = {
                    new ConstructorArgument("left", elts[i].Left , true),
                    new ConstructorArgument("top", elts[i].Top, true)
            };

            // Replace the Activator.CreateInstance                       new Ninject.Parameters.Parameter[0]
            var viewmodel = resolutionRoot.TryGet(elts[i].viewmodel, param);
            var view = ViewLocator.LocateForModel(viewmodel, null, null);

            ViewModelBinder.Bind(viewmodel, view, null);
            MyCockpitViewModels.Add((BaseViewModel)viewmodel);

now i have that in my SecondView.xaml:

<Viewbox x:Name="MainGrid" >

    <ItemsControl ItemsSource="{Binding MyCockpitViewModels}" Width="1725" Height="800">

        <ItemsControl.Resources>
            <DataTemplate DataType="{x:Type vm:SwitchOffOn_ViewModel}">
                <vm:SwitchOffOn_View />
            </DataTemplate>
            <DataTemplate DataType="{x:Type vm:SwitchOn_Off_On_ViewModel}">
                <vm:SwitchOn_Off_On_View />
            </DataTemplate>
        </ItemsControl.Resources>

        <!-- Replace panel with a canvas -->
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <!-- Set position of each element in the canvas -->
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="{x:Type ContentPresenter}">
                <Setter Property="Canvas.Left" Value="{Binding UCLeft}" />
                <Setter Property="Canvas.Top" Value="{Binding UCTop}" />
            </Style>
        </ItemsControl.ItemContainerStyle>

    </ItemsControl>
</Viewbox>

hum if i replace the definition of DataTemplate by

    <ItemsControl ItemsSource="{Binding MyCockpitViewModels}" Width="1725" Height="800">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ContentControl cal:View.Model="{Binding .}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>

i dont need to define each control in datatemplate, caliburn seems to do the job..

1 Answers

1
Mark Feldman On Best Solutions

Grid is the wrong panel to be using here. If you're designing a cockpit then you already know the position of the elements, so you don't need WPF to do the layout for you. Ergo, use Canvas. You'll probably want your cockpit to scale with the view, so give it a size based on an arbitrary unit of your own choosing (e.g. 1000x1000) and wrap the whole thing in a Viewbox.

With respect to the actual rendering of your elements you're rendering a list of controls on the screen, whenever you do that your first instinct should be to use an ItemsControl. So you would start with some type of base view model for your cockpit elements, and you would place them all in a list. To display them you use an ItemsControl, binding the ItemsSource to your list. You want to display all your controls on a Canvas, so template the ItemControl's ItemsPanel. Finally, you need to specify the location of each element on the screen, so give your base view model class X/Y properties and bind to those in the ItemControl's ItemContainerStyle. Put all that together and you get this:

<Viewbox>
    <ItemsControl ItemsSource="{Binding MyCockpitViewModels}" Width="1000" Height="1000">

        <!-- Replace panel with a canvas -->
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <!-- Set position of each element in the canvas -->
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="{x:Type ContentPresenter}">
                <Setter Property="Canvas.Left" Value="{Binding X}" />
                <Setter Property="Canvas.Top" Value="{Binding Y}" />
            </Style>
        </ItemsControl.ItemContainerStyle>

    </ItemsControl>
</Viewbox>

If you run this you'll see the names of the viewmodels being displayed at their respective canvas positions, so the only thing left to do is somehow tell WPF which controls to draw for each element instead of the text. That's done with DataTemplates, which you can place anywhere in the visual tree e.g. the app.xaml's ResourceDictionary, or the one for your MainWindow, or better yet in your ItemsControl's Resources block:

<DataTemplate DataType="{vm:SwitchOffOn_ViewModel}">
    <controls:SwitchOffOn_View />
</DataTemplate>

It's been a little while since I used Micro, it might actually do this DataTemplating for you, but if not then declare it explicitly as I've shown here for each of your controls and you'll have a fully-rendered cockpit.