Prism/MVVM (MEF/WPF): Exposing navigation [Menu's for example] from modules
I am starting my first foray into the world of Prism v4/MVVM with MEF & WPF. I have sucessfully built a shell and, using MEF, I am able to discover and initialise modules. I am however unsure as to the correct way to provide navigation to the开发者_开发技巧 views exposed by these modules.
For example, let's say that one of the modules exposes three views and I want to display navigation to these views on a menu control. So far, I have sucessfully exposed a view based upon a MenuItem
and this MenuItem
contains child MenuItem
controls thus providing a command heirarchy that can be used. Great.
Thing is, this feels wrong. I am now stating within my module that navigation (and therefore the shell) MUST support the use of menu's. What if I wanted to change to using a ToolBar
or even a Ribbon
. I would then have to change all of my modules to expose the corresponding control types for the shell.
I have looked around and there is mention on some sites of using a "Service" to provide navigation, whereby during the initialisation of the module, navigation options are added to the service which in turn is used by the shell to display this navigation in whatever format it wants (ToolBar
, TreeView
, Ribbon
, MenuItem
etc.) - but I cannot find any examples of actually doing this.
To put all of this into perspective, I am eventually looking to be able to select views from a menu and/or other navigation control (probably a Ribbon
) and then open those views on demand within a TabControl. I have already gotten as far as being able to create the views in the TabControl
at module initialisation time, now I need the next step.
What I need to know is this: what would be the correct way to expose navigation options in such a way as not the insist on support of a specific control by the shell, and if a service is the way to go then how would one put this together within the Prism/MVVM patterns.
Thanks in advance for any insight you can offer.
I suppose you have a main module containing common interfaces. You could create a simple interface like
public interface IMenuService {
void AddItem(string name, Action action);
IEnumerable<MenuItemViewModel> GetItems { get; }
}
Create 1 implementation and a single instance.
public class MenuService : IMenuService {
private readonly IList<MenuItemViewModel> items = new List<MenuItemViewModel>();
void AddItem(string name, Action action) {
items.Add(new MenuItemViewModel {
Name = name,
Action = action
});
}
IEnumerable<MenuItemViewModel> GetItems {
get { return list.AsEnumerable(); }
}
}
Within your modules, use MEF to resolve this instance and call AddItem()
to register your views.
The Action
property is a simple delegate to activate a view or do anything else.
Then in your shell or any view, you just need to call the GetItems
property to populate your menu.
Having thought about this some more, I have come to the following conclusion that I feel affects the way that I need to deal with this...
The modules need to be partially aware of the shell layout anyway - that is, the shell exposes a number of regions and the modules need to be aware of those regions (by name as well as what is expected to be shown) in order to populate them correctly when functionality is requested (either by means of registering a view within a region or as the reaction to a user action).
Because of this, the modules need to be designed to interact with the shell to place content into the named regions and as such, I see no reason why the modules should not expose whatever type of navigation the shell supports.
Therefore, my modules (currently) expose a "RibbonView" (a RibbonTab) with the necessary icons, buttons and commands etc to expose the functionality of the module. Each "RibbonView" is registered with the "RibbonRegion" of the shell, along with hints for ordering, and this is then rendered within the shell.
If in the future I choose to update my shell to use the latest+greatest navigation control (whatever that may be in x years time) then I simply need to update each of the modules to expose the necessary items to integrate with that new navigation and, because I am loading into a new shell, I can then update my view registration accordingly.
I just hope that I am not breaking any of the principles of the composite application in doing this, but that said I have never yet found a pattern that can actually be implemented in a real scenario without some "interpretation".
I would be interested to hear if anybody has any opinions on this.
I've encountered the same situation, and I think the solution lies in differentiating between interface and implementation. For example, you can design a view in a module that performs a given function. That's all it does. As soon as you use or consume this in a specific context you've crossed over into implementation. Now, ideally the view is unaware of how it's being implemented, and certainly would have no knowledge of named Regions in the Shell. So, seating views into Regions within a module is a no-no.
To get around this, I've opted to delegate this responsibility to a third-party component, a LayoutManager. The LayoutManager sits between the Shell and Module and defines "what goes where". It is a specific implementation, and really defines the implementation. Both the Shell and the Module view remain generic.
Have a look at: http://rgramann.blogspot.com/2009/08/layout-manager-for-prism-v2.html
Which may give you some ideas around this problem.
Hope it helps.
This article uses an abstraction (IMenuItem
) to represent the ViewModels for your menu choices. How you actually render these imported objects is really up to the host application. The example uses a WPF menu, but you could certainly render it any way you wanted because IMenuItem
is abstract enough.
If you changed IMenuItem
to INavigationItem
, you've got what you want.
In that article, when the particular navigation item gets notified that it's been "run", then it usually instantiates a ViewModel for a document or "pad", and passes that to the ILayoutManager service. It has a pluggable architecture, so you can swap out the LayoutManager service for a different layout engine (the default one is a wrapper around AvalonDock).
精彩评论