ViewModels references in ShellViewModel Caliburn.M

2019-05-30 15:40发布

问题:

In this thread : Can anybody provide any simple working example of the Conductor<T>.Collection.AllActive usage? I've had part of an answer but I'm still a but confused.

I would simply like to reference all my view models into my ShellViewModel to be able to open/close ContentControls, but without injecting all of them in the constructor.

In the answer, it is suggested to inject an interface in the constructor of the ShellViewModel. If I do that, do I have to inject all my ViewModels in a class that implements that interface?

public MyViewModel(IMagicViewModelFactory factory)
{
    FirstSubViewModel = factory.MagicallyGiveMeTheViewModelIWant();
    SecondSubViewModel = factory.MagicallyGiveMeTheViewModelIWant();
    ThirdSubViewModel = factory.MagicallyGiveMeTheViewModelIWant();

    Items.Add(FirstSubViewModel);
    Items.Add(SecondSubViewModel);
    Items.Add(ThirdSubViewModel);
}

Also, I would like to avoid going through IoC.Get<> to get the instances of my view Models, I think it violates the principles of IoC if I am not mistaken. In a few other examples, they create new viewModels when needed, but what's the point of using IoC in that case, especially when I need services injected inside those new ViewModels?

In my Shell view, I have a layout with 3 different areas, bound to my shell view model by :

 <ContentControl x:Name="Header"
                    Grid.ColumnSpan="3"/>
 <ContentControl x:Name="Menu"
                    Grid.Row="1"/>
 <ContentControl x:Name="Main"
                    Grid.ColumnSpan="3"/>

In my ShellViewModel extending Conductor.Collection.AllActive, I reference the 3 areas like this:

public Screen Menu { get; private set; }
public Screen Header { get; private set; }
public Screen Main { get; private set; }

I would like to be able to change them like so:

Menu = Items.FirstOrDefault(x => x.DisplayName == "Menu");
Header = Items.FirstOrDefault(x => x.DisplayName == "Header");
Main = Items.FirstOrDefault(x => x.DisplayName == "Landing");

All my ViewModels have a DisplayName set in their constructor.

I have tried this but GetChildren() is empty

foreach (var screen in GetChildren())
        {
            Items.Add(screen);
        }

Am I missing something obvious?

Thanks in Advance!

回答1:

Finally, I managed to find an answer myself. It's all in the AppBootstrapper!

I ended up creating a ViewModelBase for my Screens so that they could all have an IShell property (so that the ViewModels could trigger a navigation in the ShellViewModel) like so:

public class ViewModelBase : Screen
{
    private IShell _shell;

    public IShell Shell
    {
        get { return _shell; }
        set
        {
            _shell = value;
            NotifyOfPropertyChange(() => Shell);
        }
    }
}

then in the AppBoostrapper registered them like this :

container.Singleton<ViewModelBase, SomeViewModel>();
container.Singleton<ViewModelBase, AnotherViewModel>();
container.Singleton<ViewModelBase, YetAnotherViewModel>();

then created an IEnumerable to pass as param to my ShellViewModel ctor:

IEnumerable<ViewModelBase> listScreens = GetAllScreenInstances();

container.Instance<IShell>(new  ShellViewModel(listScreens));

then passing the IShell to each ViewModels

foreach (ViewModelBase screen in listScreens)
{
    screen.Shell = GetShellViewModelInstance();
}

for the sake of completeness, here are my GetAllScreenInstances() and GetShellViewModelInstance() :

protected IEnumerable<ViewModelBase> GetAllScreenInstances()
{
     return container.GetAllInstances<ViewModelBase>();
}
protected IShell GetShellViewModelInstance()
{
    var instance = container.GetInstance<IShell>();
    if (instance != null)
        return instance;

    throw new InvalidOperationException("Could not locate any instances.");
}

Here's what my ShellViewModel ctor looks like:

public ShellViewModel(IEnumerable<ViewModelBase> screens )
{         
    Items.AddRange(screens);
}

Hope this can help someone in the future!