Mapping derived ViewModels to base class View in Caliburn.Micro - c#

I have a base ViewModel and associated View. I also have multiple derived ViewModels from the base ViewModel, but I'd like to use the base View for display.
Base ViewModel and View:
vm: MyCompany.MyApp.Modules.Wizard.ViewModels.WizardViewModel
vw: MyCompany.MyApp.Modules.Wizard.Views.WizardView
Derived from WizardViewModel:
vm: MyCompany.MyApp.Modules.NewSpec.ViewModels.NewSpecViewModel : WizardViewModel
vw: (map to MyCompany.MyApp.Modules.Wizard.Views.WizardView)
vm: MyCompany.MyApp.Modules.NewSpec.ViewModels.NewMaterialViewModel : WizardViewModel
vw: (map to MyCompany.MyApp.Modules.Wizard.Views.WizardView)
I think this should be possible using the mapping in ViewLocator or ViewModelLocator or NameTransformer, but I haven't figured it out yet.
I am using the Gemini Framework with Caliburn.Micro v1.5.2 (I plan on upgrading to v2 soon).
Here is one of the things I have tried:
public class NewSpecViewModel : WizardViewModel
{
// ...
static NewSpecViewModel()
{
// Escape the '.' for the regular expression
string nsSource = typeof(NewSpecViewModel).FullName.Replace(".", #"\.");
string nsTarget = typeof(WizardViewModel).FullName;
nsTarget = nsTarget.Replace("WizardViewModel", "Wizard");
// nsSource = "MyCompany\\.MyApp\\.Modules\\.NewSpec\\.ViewModels\\.NewSpecViewModel"
// nsTarget = "MyCompany.MyApp.Modules.Wizard.ViewModels.Wizard"
ViewLocator.AddTypeMapping(nsSource, null, nsTarget);
}
// ...
}
P.S. I know there are existing Wizard frameworks (Extended WPF Toolkit, Avalon Wizard, etc), but I don't want to add another 3rd party assembly and the Extended WPF Toolkit Wizard wasn't working properly.
P.P.S. I also want to use this style of base ViewModel/View mapping elsewhere.

Here's [a link] (https://caliburnmicro.codeplex.com/discussions/398456) to right way to do this.
EDIT: Since codeplex is shutting down, here is the code from the discussion:
var defaultLocator = ViewLocator.LocateTypeForModelType;
ViewLocator.LocateTypeForModelType = (modelType, displayLocation, context) =>
{
var viewType = defaultLocator(modelType, displayLocation, context);
while (viewType == null && modelType != typeof(object))
{
modelType = modelType.BaseType;
viewType = defaultLocator(modelType, displayLocation, context);
}
return viewType;
};

I know it's late...
but there is an option to bind the ViewModel to a view directly, and maybe this helps others.
I would also append this binding to the base classes constructor. The following works for me:
public abstract class WizardViewModel {
protected WizardViewModel() {
// this --> points the child class
ViewModelBinder.Bind(this, new WizardView(), null);
}
}
With this, each child now uses the WizardView (without any additional programming in the child class).
public class NewSpecViewModel : WizardViewModel {}

Related

Advice on Views navigation using Caliburn.Micro MVVM WPF

I'm new on Caliburn Micro and want some advice on which path to take to devolop my app interface and navigation between views.
My idea is to have a MainWindow which will contain a menu of buttons, each one related with a specific view. Each view will be stored in a separated WPF UserControl. The mainWindow will also contain a TabControl bound to an ObservableCollection of tabs on viewmodel. Everytime a button on menu is clicked, I want to add a new tab with a ContentPresenter inside that will dynamically load a view and its corresponding viewmodel.
So my questions:
1) Should I use a Screen Collection here?
2) Should the UserControl implement Screen interface?
3) How do I tell MainWindow ViewModel which view to load on the new added tab maintaining viewmodels decoupled?
Thanks to everyone in advance.
UPDATE
After a lot of reading and some help of the community I managed to resolve this. This is the resultant AppViewModel:
class AppViewModel : Conductor<IScreen>.Collection.OneActive
{
public void OpenTab(Type TipoVista)
{
bool bFound = false;
Screen myScreen = (Screen)Activator.CreateInstance(TipoVista as Type);
myScreen.DisplayName = myScreen.ToString();
foreach(Screen miItem in Items)
{
if (miItem.ToString() == myScreen.ToString())
{
bFound = true;
ActivateItem(miItem);
}
}
if (!bFound) ActivateItem(myScreen);
}
public ObservableCollection<MenuItem> myMenu { get; set; }
public ObservableCollection<LinksItem> myDirectLinks { get; set; }
public ICommand OpenTabCommand
{
get
{
return new RelayCommand(param => this.OpenTab((Type) param), null);
}
}
public AppViewModel()
{
OpenTab(typeof(ClientsViewModel));
MenuModel menu = new MenuModel();
myMenu = menu.getMenu();
myDirectLinks = menu.getLinks();
}
public void CloseTab(Screen param)
{
DeactivateItem(param, true);
}
}
I have to keep the ICommand from OpenTabCommand because the name convention of Caliburn.micro doesn't seems to work inside DataTemplate. Hope it could help someone else. Thanks to all
I've done something very similar using Caliburn.Micro, and based it on the SimpleMDI example included with the examples, with a few tweaks to fit my needs.
Much like in the example, I had a main ShellViewModel:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
{
}
with a corresponding ShellView containing a TabControl - <TabControl x:Name="Items">, binding it to the Items property of the the Conductor.
In this particular case, I also had a ContextMenu on my ShellView, bound (using the Caliburn.Micro conventions), to a series of commands which instantiated and Activated various other ViewModels (usually with a corresponding UserControl, using the ActivateItem method on the Conductor.
public class YourViewModel: Conductor<IScreen>.Collection.OneActive
{
// ...
public void OpenItemBrowser()
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
In that case, I didn't require the ViewModels to be created with any particular dependency, or from any other locations in the program.
At other times, when I've needed to trigger ViewModel from elsewhere in the application, I've used the Caliburn.Micro EventAggregator to publish custom events (e.g. OpenNewBrowser), which can be handled by classes implementing the corresponding interface (e.g. IHandle<OpenNewBrowser>), so your main ViewModel could have a simple Handle method responsible for opening the required View:
public class YourViewModel: Conductor<IScreen>.Collection.OneActive, IHandle<OpenNewBrowser>
{
// ...
public void Handle(OpenNewBrowser myEvent)
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
This section of the documentation will probably be useful, especially the Simple MDI section.
Additional code I mentioned in the comments:
I sometimes use a generic method along these lines ensure that if I have an existing instance of a screen of a particular type, switch to it, or create a new instance if not.
public void ActivateOrOpen<T>() where T : Screen
{
var currentItem = this.Items.FirstOrDefault(x => x.GetType() == typeof(T));
if (currentItem != null)
{
ActivateItem(currentItem);
}
else
{
ActivateItem(Activator.CreateInstance<T>());
}
}
Used like:
public void OpenBrowser()
{
this.ActivateOrOpen<BrowserViewModel>();
}

Using one view model in multiple windows

How can I use one view model for many windows in WPF? I need model to be updated from one window only and handle these changes in others (for ex. property 'Locked').
I have one view model incapsulating the most general info that should be used not only on the A (suppose it is 'Company') window but also on windows child window B (suppose it is 'Person'). So the 'General' view model should be determined by A entity but be passes to all children entity. While updating this view model on A window - we should se changes on all B windows.
public partial class A : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
}
public partial class B : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
public B(GeneralViewModel g)
{
this.general = g;
}
}
I wish the model should be updated only in A and B was simply displaying that changes were maid. In case I pass model as it is shown in this code or if I implement 'General' as property with getter and setter changes are not applied.
Thanks for any help.
You could use a singleton-class as your ViewModel.
Example:
public Window()
{
this.DataContext = ViewModel.Instance.
}
EDIT:
public GeneralViewModel
{
public DataType Model
{
get { return DataType.Instance; }
}
}
Now everytime you access the Model in one of your GeneralViewModels, it is locked for all others.
Initialise your view model in a static member somewhere and have the windows return the value as their GeneralViewModel.
In the scenario you mentioned, your GeneralViewModel is a kind of Dependency to both your Window classes and for these purposes you can use some of the available IoC containers or MEF built into .Net 4. You can register your Dependencies including the GeneralViewModel in some application startup event.
Below is some sample code that will make your GeneralViewModel instance to be then located from the container it is registered with (MEF in below case):
[Export(typeof(B))]
public partial class B : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
[ImportingConstructor]
public B(GeneralViewModel g)
{
this.general = g;
}
}
To learn more about MEF,see these articles:
CodePlex
Ten Reasons to use the Managed Extensibility Framework
Managed Extensibility Framework Overview
An Introduction to Managed Extensibility Framework (MEF) - Part I
There are many other DI and IoC containers available as Open Source downloads.
There is no problem if you use MVVM. In this case your ViewModel will correspond to some View which is basically the UserControl and you can place it to as many Windows as you wish.
And When you implement MVVM you should also use INotifyPropertyChanged or ObservableCollections

How to activate corresponding screen in Caliburn.Micro

1.In my Silverlight project, I've got several Plugins (inheriting IPlugin and IScreen) and import them into the ShellView (main view) using MEF.
2.Then I bind the metadata (I've defined myself, including some basic descriptions of a Plugin) of Plugins to a ListBox.
Now I want the ContentControl to load the viewmodel corresponding to the selcted plugin (PluginMetadata to be exactly) in the ListBox. The problem is the viewmodel has to be determined and instantiated at runtime. I've searched a lot but it seems that people usually activate the viewmodel which is already determined at design time. For example:
ActivateItem(new MyContentViewModel());
or:
<ContentControl x:Name="MyContent" cal:View.Model="{Binding Path=MyContentViewModel}" />
One idea that came to my mind was to determine the type corresponding to the plugin by defining an attribute in my PluginMetadata class and use it like this:
[Export(IPlugin)]
[PluginMetadata(Type=typeof(Plugin1), ...some other properties...)]
public class Plugin1 {...}
And load the viewmodel with an instance of the plugin created using Reflection.
ActivateItem(Activator.CreateInstance<SelectedPluginMetadata.Type>());
or maybe I can also use binding if I add a property SelectedPluginType:
<ContentControl x:Name="MyContent" cal:View.Model="{Binding Path=SelectedPluginType}" />
However, passing the type in the metadata attribute seems so ungraceful and against DRY.
So is there any better solution?
Ok so instead:
The ViewLocator exposes this delegate which you can replace with your own:
public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) => {
var viewType = LocateTypeForModelType(modelType, displayLocation, context);
return viewType == null
? new TextBlock { Text = string.Format("Cannot find view for {0}.", modelType) }
: GetOrCreateViewType(viewType);
};
So I'd probably just stick this in your Bootstrapper.Configure:
ViewLocator.LocateForModelType = (modelType, displayLocation, context) =>
{
if(modelType is IPlugin) modelType = ? // whatever reflection is necessary to get the underlying type? Just GetType()?
var viewType = ViewLocator.LocateTypeForModelType(modelType, displayLocation, context);
return viewType == null
? new TextBlock { Text = string.Format("Cannot find view for {0}.", modelType) }
: ViewLocator.GetOrCreateViewType(viewType);
};

How to make this DataTemplateSelector work?

In my Viewmodel I have the properties LoggedInAs of type string and EditMode of type bool. I also have a List property called ReaderList which I bind to an ItemsControl for display purposes like this:
<ItemsControl Name="ReaderList" ItemTemplateSelector="{StaticResource drts}"/>
I am using Caliburn.Micro, so the Binding is done automatically by the naming. I want to use a DataTemplateSelector because if the application is in EditMode and the Person is the one that is logged in I want a fundamentally different display. So here is my declaration of the resources,
<UserControl.Resources>
<DataTemplate x:Key="OtherPersonTemplate"> ... </DataTemplate>
<DataTemplate x:Key="CurrentUserIsPersonTemplate"> ... </DataTemplate>
<local:DisplayReaderTemplateSelector x:Key="drts"
IsLoggedInAs="{Binding LoggedInAs}"
IsEditMode="{Binding EditMode}"
CurrentUserTemplate="{StaticResource CurrentUserIsPersonTemplate}"
OtherUserTemplate="{StaticResource OtherPersonTemplate}"/>
</UserControl.Resources>
and here the code for the class:
public class DisplayReaderTemplateSelector: DataTemplateSelector {
public DataTemplate CurrentUserTemplate { get; set; }
public DataTemplate OtherUserTemplate { get; set; }
public string IsLoggedInAs {get; set;}
public bool IsEditMode { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container){
var _r = item as Person;
if (IsEditMode && _r.Name == IsLoggedInAs) return CurrentUserTemplate;
else return OtherUserTemplate;
}
}
For some reason the application crashes while instantiating the Viewmodel (resp. the View). Where is the error, and/or how could I solve this problem alternatively?
EDIT: The Crash was due to the binding expressions in the construction of the DisplayReaderTemplateSelector - because IsLoggedIn and EditMode are not DependencyProperties.
So the question now is: how can I have a DataTemplateSelector that depends on the status of the ViewModel if I cannot bind to values?
Whilst you could use a DataTemplateSelector or something of that ilk, it probably won't surprise you to find that in Caliburn.Micro has this functionality built-in in the form of View.Context and the ViewLocator
On your VM you can create a property which provides a context string which CM will use to resolve the View - since it uses naming conventions, you just need to provide the correct namespace/name for the sub-view along with a context string for it to locate an alternative view
In your VM you can create a context property that uses the user details to determine its value:
i.e.
public class SomeViewModel
{
public string Context
{
get
{
if (IsEditMode && _r.Name == IsLoggedInAs) return "Current";
else return "Other";
}
}
// ... snip other code
}
The only problem I see (one that probably has a workaround) is that you want to determine the view from inside a ViewModel - usually you determine the context higher up and pass that to a ContentControl and CM uses it when locating the view for that VM
e.g.
your main VM:
public class MainViewModel
{
public SomeSubViewModel { get; set; } // Obviously would be property changed notification and instantiation etc, I've just left it out for the example
}
and associated view
<UserControl>
<!-- Show the default view for this view model -->
<ContentControl x:Name="SomeSubViewModel" />
<!-- Show an alternative view for this view model -->
<ContentControl x:Name="SomeSubViewModel" cal:View.Context="Alternative" />
</UserControl>
then your VM naming structure would be:
- ViewModels
|
----- SomeSubViewModel.cs
|
- SomeSubView.xaml
|
- SomeSubView
|
----- Alternative.xaml
and CM would know to look in the SomeSubView namespace for a control called Alternative based on the original VM name and the Context property (SomeSubViewModel minus Model plus dot plus Context which is SomeSubView.Alternative)
So I'd have to have a play around as this is the standard way of doing it. If you were to do it this way you'd have to either create a sub viewmodel and add a ContentControl to your view and bind the View.Context property to the Context property on the VM, or add the Context property higher up (to the parent VM).
I'll look at some alternatives - if there is no way to get the current ViewModel to decide its view based on a property using standard CM, you could customise the ViewLocator and maybe use an interface (IProvideContext or somesuch) which provides the ViewLocator with a context immediately -(I don't think you can't hook directly into the view resolution process from a VM)
I'll come back with another answer or an alternative shortly!
EDIT:
Ok this seems to be the most straightforward way to do it. I just created an interface which provides Context directly from a VM
public interface IProvideContext
{
string Context { get; }
}
Then I customised the ViewLocator implementation (you can do this in Bootstrapper.Configure()) to use this if no context was already specified:
ViewLocator.LocateForModel = (model, displayLocation, context) =>
{
var viewAware = model as IViewAware;
// Added these 3 lines - the rest is from CM source
// Try cast the model to IProvideContext
var provideContext = model as IProvideContext;
// Check if the cast succeeded, and if the context wasn't already set (by attached prop), if we're ok, set the context to the models context property
if (provideContext != null && context == null)
context = provideContext.Context;
if (viewAware != null)
{
var view = viewAware.GetView(context) as UIElement;
if (view != null)
{
#if !SILVERLIGHT && !WinRT
var windowCheck = view as Window;
if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
{
LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
return view;
}
#else
LogManager.GetLog(typeof(ViewLocator)).Info("Using cached view for {0}.", model);
return view;
#endif
}
}
return ViewLocator.LocateForModelType(model.GetType(), displayLocation, context);
};
This should work for you and allows you to set the context directly on the target ViewModel - obviously this will probably only work for a View-First approach
So all you need to do is structure your views as I showed above (the correct namespaces etc) then set the Context property on your VM based on the value of IsLoggedInAs and EditMode

DSL Custom Constructor - only calling when created not loading

Info: VS2010, DSL Toolkit, C#
I have a custom constructor on one of my domain classes which adds some child elements. I have an issue as I only want this to run when the domain class element is created , not every time the diagram is opened (which calls the construtors)
public Entity(Partition partition, params PropertyAssignment[] propertyAssignments)
: base(partition, propertyAssignments)
{
if (SOMETHING_TO_STOP_IT_RUNNING_EACH_TIME)
{
using (Transaction tx = Store.TransactionManager.BeginTransaction("Add Property"))
{
Property property = new Property(partition);
property.Name = "Class";
property.Type = "System.String";
this.Properties.Add(property);
this.Version = "1.0.0.0"; // TODO: Implement Correctly
tx.Commit();
}
}
}
It looks like you are initializing some domain class properties from within the constructor. This is best done by creating an AddRule. AddRules are invoked when an instance of the domain class to which they are attached is added to the model. For example :
[RuleOn(typeof(Entity), FireTime = TimeToFire.TopLevelCommit)]
internal sealed partial class EntityAddRule : AddRule
{
public override void ElementAdded(ElementAddedEventArgs e)
{
if (e.ModelElement.Store.InUndoRedoOrRollback)
return;
if (e.ModelElement.Store.TransactionManager.CurrentTransaction.IsSerializing)
return;
var entity = e.ModelElement as Entity;
if (entity == null)
return;
// InitializeProperties contains the code that used to be in the constructor
entity.InitializeProperties();
}
}
The AddRule then needs to be registered by overriding a function in your domain model class:
public partial class XXXDomainModel
{
protected override Type[] GetCustomDomainModelTypes()
{
return new Type[] {
typeof(EntityAddRule),
}
}
}
For more information about rules, have a look at the "How to: Create Custom Rules" topic in the VS SDK documentation.
Note: the solution is based on the VS 2008 DSL Tools. YMMV.
Although not the correct approach (Paul Lalonde answer is the best),
here's how you may know, at any given time, if the model is being serialized (= loading):
this.Store.TransactionManager.CurrentTransaction!= null &&
this.Store.TransactionManager.CurrentTransaction.IsSerializing

Categories

Resources