How to activate corresponding screen in Caliburn.Micro - c#

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);
};

Related

How can I test a ViewModel that needs to coordinate the creation and hosting of UserControls?

I'm working on a demo MVVM project where I have a WPF MainWindow with a ViewModel that needs to coordinate the creation and hosting of different UserControls. If the ViewModel is not supposed to have any part of WPF elements I'm not sure how to go about doing this. I know this is a rather broad design question but I'm new to WPF/TDD and I'm having difficulty seeing a clear path as to how to create and bind a UserControl to a ViewModel without having some of the create and bind code IN the ViewModel.
From what I've read exposing a UserControl property in the MainViewModel that binds to a ContentControl is not the way to go. How can I abstract away the creation and binding of UserControls in my MainView model so I can test it?
Works but not testable:
<ContentControl Grid.Row="2" Content="{Binding UserControl}" />
public class MainWindowViewModel
{
public void ShowHome()
{
SomeUserControl uc = new SomeUserControl();
uc.DataContext = new SomeUserControlViewModel();
UserControl = uc;
}
public void ShowKeypad()
{
SomeOtherUserControl uc = new SomeOtherUserControl();
uc.DataContext = new SomeOtherUserControlViewModel();
UserControl = uc;
}
public UserControl UserControl {get; private set;}
}
simply use a DataTemplate. Let Wpf choose the View for you.
<ContentControl Grid.Row="2" Content="{Binding UserControl}" />
public class MainWindowViewModel
{
public void ShowHome()
{
MyViewmodelChoosedInMain = new SomeViewModel();
}
public void ShowKeypad()
{
MyViewmodelChoosedInMain = new SomeOtherViewModel();
}
//better use an Interface instead of object type ;)
//you also need to implement and call INotifyPropertyChanged of course
public object MyViewmodelChoosedInMain {get; private set;}
}
//in your ResourceDictionary create the DataTemplates
<DataTemplate DataType="{x:Type SomeViewModel}">
<MySomeViewmodelView />
</DataTemplate>
<DataTemplate DataType="{x:Type SomeOtherViewModel}">
<MySomeOtherViewmodelView />
</DataTemplate>
There are a couple of things you can do.
I have created many projects where the view at startup creates controls where the visibility is set to Hidden. Then the VM creates/has a State property which defines the different states of the app. As that property is changed (via INotifyPropertyChanged) controls on the screen appear or hide themselves.
Working with #1 or without, one can create commands that the view can process but which are initiated by the VM or elsewhere. Hence keeping separation of concerns.
#1
Define Enum
public enum OperationState
{
Select = 1,
Routine,
Alignment,
SerialNumber,
}
On VM define state
private OperationState _State;
public OperationState State
{
get => _State;
set { _State = value; OnPropertyChanged(nameof(State)); }
}
Set state as needed such as State = Select.
Have control(s) viewability based on state
<Control:AlignmentProcessing
ProjectContainer="{Binding CurrentContainer, Mode=TwoWay}"
Visibility="{Binding State, Converter={StaticResource VisibilityStateConverter},
ConverterParameter=Alignment}"/>
The above control will only be visible during the Alignment state.
Converter Code
/// <summary>
/// Take the current state, passed in as value which is bound, and check it against
/// the parameter passed in. If the names match, the control should be visible,
/// if not equal, then the control (window really) should be collapsed.
/// </summary>
public class OperationStateToVisibility : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value != null) &&
(parameter != null) &&
value.ToString().Equals(parameter.ToString(), StringComparison.OrdinalIgnoreCase)
? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
#2 Otherwise implement a commanding operation on the VM such as:
public ICommand ShowControl1 { get; set; }
Then on the View subscribe to the command (assuming VM holds the current VM):
VM.ShowControl1 = new Commanding((o) =>
{
SomeUserControl uc = new SomeUserControl();
uc.DataContext = new SomeUserControlViewModel();
UserControl = uc;
}
Then when the command is executed (from the VM) the view does its work.
ShowControl1.Execute(null);
I provide a commanding example on my blog Xaml: MVVM Example for Easier Binding
The view-model should not have any controls in it with the MVVM pattern. A view-model is simply a state of the data to be displayed.
For example:
Record(s) to display
Title
Image
Can Delete
Can Edit
In WPF, the controls can bind to these properties for it's display.
View-Models are view agnostic (they don't care which view is consuming it). And so no actual reference to UI controls would be in them.
To test the actual UI, you can write a "coded UI test". In the web app works, there is the Selenium framework that allowed you to write unit tests for interacting with UI components in a browser.
I'm pretty sure there is a similar framework it there for WPF UI testing.
Edit: There is a framework called Appium that allows you to write the integration tests between your UI and the underlying MVVM setup you have.
http://appium.io/docs/en/drivers/windows/

How to set the model in a viewmodel (MVVM)

I have an HelloWorldWPFApplication class with the following method:
public override void Run()
{
var app = new System.Windows.Application();
app.Run(new ApplicationShellView());
}
The ApplicationShellView has the following XAML:
<winbase:ApplicationShell x:Class="HelloWorldWPFApplication.View.ApplicationShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:winbase="clr-namespace:Framework.Presentation.Control.Window;assembly=Framework"
xmlns:vm="clr-namespace:HelloWorldWPFApplication.ViewModel"
Title="{Binding WindowTitle, Mode=OneWay}">
<Window.DataContext>
<vm:ApplicationShellViewModel />
</Window.DataContext>
</winbase:ApplicationShell>
If my ViewModel (ApplicationShellViewModel) has the following method, the window will have the title set to "Test":
public string WindowTitle
{
get
{
return "Test";
}
}
My problem is that I want to set the title based on properties within the HelloWorldWPFApplication class. I added the following to the HelloWorldWPFApplication's base class (which uses the INotifyPropertyChanged interface):
private WpfApplicationBase<WpfApplicationDataBase> applicationModel;
public WpfApplicationBase<WpfApplicationDataBase> Application
{
get { return this.applicationModel; }
set { this.Set<WpfApplicationBase<WpfApplicationDataBase>>(ref this.applicationModel, value); }
}
So effectively, I plan on reusing the existing HelloWorldWPFApplication object as the model (in MVVM).
I changed the WindowTitle property as follows:
public string WindowTitle
{
get
{
return String.Format("{0} {1}",
this.applicationModel.Data.FullName,
this.applicationModel.Data.ReleaseVersion).Trim();
}
}
Of course, at this stage my project creates a window without a title, as the application field has not been set. I don't want to create a new application object within the view model as one already exists. I want to use this existing object. What is the best way to achieve this?
I am very new to MVVM/WPF - and from my basic understanding of MVVM I don't want to put any code-behind in the view. I could have a static field set on a static class to the application object, and then assign this field in my view model (this works, but not sure having "global" variables is the best approach).
I have also tried creating the view model before showing the window, but have encountered a problem I have yet to solve. In this implementation my run method appears as follows:
public override void Run()
{
var window = new ApplicationShell(); // inherits from System.Windows.Window
var vm = new ApplicationShellViewModel();
vm.Application = this; // this line won't compile
window.DataContext = vm;
this.Data.WpfApplication.Run(window);
}
I get a compile error:
Error 1 Cannot implicitly convert type 'HelloWorldWPFApplication.Program.HelloWorldApplication' to 'Framework.Business.Logic.Program.Application.WpfApplicationBase'
I'm confused with the error as my HelloWorldWPFApplication class inherits from WpfApplicationBase:
public class HelloWorldApplication<T> : WpfApplicationBase<T>
where T : HelloWorldApplicationData
Additionally, HelloWorldApplicationData inherits from WpfApplicationDataBase.
I get the pretty much the same problem with the following implementation:
public override void Run()
{
var window = new ApplicationShell();
var vm = new ApplicationShellViewModel();
var app = new HelloWorldApplication<HelloWorldApplicationData>();
vm.Application = app; // Cannot implicitly convert type error again
window.DataContext = vm;
this.Data.WpfApplication.Run(window);
}
Exact error:
Error 1 Cannot implicitly convert type 'HelloWorldWPFApplication.Program.HelloWorldApplication' to 'Framework.Business.Logic.Program.Application.WpfApplicationBase'
First off, the "Application" class in WPF should be used for one thing, and one thing only: starting the program. It is not a model.
That said, I would just pass everything in sequence (this can apply to a proper model as well):
MyViewModel viewmodel = new MyViewModel(this);
var app = new System.Windows.Application();
app.Run(new ApplicationShellView(viewmodel));
Of course, remove the data context set from XAML. This does require modifying your code behind to accept the VM object and set it to the DataContext in your constructor, but thats a standard way of passing the VM to the View.
You could also use a Service Locator to find your model, or a number of other ways. Unfortunately, its hard to say which one is right, since your model is so weird.
As a complete aside; the title of your program is very much a part of the View, and probably doesn't need to be bound at all (your name is static, so making your application class the model isn't buying you anything).

Mapping derived ViewModels to base class View in Caliburn.Micro

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 {}

Make MVC not post back one of the members in the model which is an interface

I have a view model that looks like this:
class MyViewModel
{
public string Stuff { get; set; }
public IFoo Foo { get; set; }
}
The GET requests all work fine because I supply both, Stuff and IFoo to the view.
However, when posting back, MVC says it can't make an object of an interface.
Now, I have two options:
a) Change the type of the Foo property to a concrete implementation, which is no big deal; or
b) Do the model binding myself, which is an overkill for what I want. I really don't care about posting back the IFoo member.
How do I tell MVC not to post back IFoo? I don't have any HTML controls also representing the IFoo. Properties from the IFoo, I use as hidden thingies in the view.
You can exclude properties from binding.
Via the model
[Bind(Exclude="Foo")]
class MyViewModel
{
public string Stuff { get; set; }
public IFoo Foo { get; set; }
}
Via the action
[HttpPost]
public ActionResult MyGreatAction([Bind(Exclude="Foo")]MyViewModel model)
{
}
I had a similar problem and found this link to be of super help to me.. Custom Model Binder But just in case the link goes dead sometime in the future here is the thought process behind it.
Posting a list of interfaces
If you try to post this form, MVC will throw this error: Cannot create an instance of an interface. This is because the default model binder works by creating an instance of your model (and any properties it has) and mapping the posted field names to it; Section.Title maps to the Title property on the Section object, for example. However, Section.SectionFields[0] presents a problem – you cannot create an instance of an interface (or an abstract class).
The solution is to create and register a custom model binder for the IField class...
First create a model binder and inherit from the DefaultModelBinder class:
public class IFieldModelBinder : DefaultModelBinder {
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType) {
// Our work here
}
}
Secondly, register it with your application – this is usually done in Application_Start in Global.asax.
protected void Application_Start(Object sender, EventArgs e) {
ModelBinders.Binders.Add(typeof(IField), new IFieldModelBinder());
}
Next cast each IField into its concrete type. I think she used reflection so she added to properties to her Interface FieldClassName, and FieldAssemblyName. Then she put in 2 hidden fields for Class name and Assembly name in her editor template.
#model SectionSummary
#Html.HiddenFor(x => x.FieldClassName)
#Html.HiddenFor(x => x.FieldAssemblyName)
Now, whenever this particular field is posted, the custom model binder will have the information about the actual type available – which means we can use it to cast the IField object and return a model that can be instantiated.
public class IFieldModelBinder : DefaultModelBinder
{
protected override object CreateModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
// Get the submitted type - should be IField
var type = bindingContext.ModelType;
// Get the posted 'class name' key - bindingContext.ModelName will return something like Section.FieldSections[0] in our particular context, and 'FieldClassName' is the property we're looking for
var fieldClassName = bindingContext.ModelName + ".FieldClassName";
// Do the same for the assembly name
var fieldAssemblyName = bindingContext.ModelName + ".FieldAssemblyName";
// Check that the values aren't empty/null, and use the bindingContext.ValueProvider.GetValue method to get the actual posted values
if (!String.IsNullOrEmpty(fieldClassName) && !String.IsNullOrEmpty(fieldAssemblyName))
{
// The value provider returns a string[], so get the first ([0]) item
var className = ((string[])bindingContext.ValueProvider.GetValue(fieldClassName).RawValue)[0];
// Do the same for the assembly name
var assemblyName =
((string[])bindingContext.ValueProvider.GetValue(fieldAssemblyName).RawValue)[0];
// Once you have the assembly and the class name, get the type - I am overwriting the IField object that came in, but I do not think you have to do that
modelType = Type.GetType(className + ", " + assemblyName);
// Finally, create an instance of this type
var instance = Activator.CreateInstance(modelType);
// Update the binding context's meta data
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, modelType);
// Return the instance - which will now be a SummaryField or CommentField - rather than an IField
return instance;
}
return null;
}
}
You will now be able to post your List object – for each IField, the custom model binder will be called to figure out what the concrete type of each object is.

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

Categories

Resources