I have a Prism 7 / WPF / MVVM app that is configured with AutowireViewModel="True" in the views that have a viewmodel. Most of my view models have dependencies that I have configured in the Prism Unity container and these dependencies are injected into the viewmodel contructor. I do not explicitly create instances of views/viewmodels in the code behind anywhere. I also do not set the data context in XAML i.e. using DataContext element or d:DataContext). The only references in the XAML are to views (i.e. part of the HamburgerMenu control).
All is working fine except each view/viewmodel is ALWAYS constructed twice for some reason, whether this is the main window, or views within modules. I have placed breakpoints in the module managers (only gets hit once) - and also in the view constructors and viewmodel constructors which are both hit twice.
The following is some code for App class and also some code/view/viewmodel for a module named Messaging that is loaded at runtime not on demand. Due to wishing to use the MVVM pattern as much as possible, I have very little code in the code behind of the views. NOTE: I tried to see if this is just an issue with views for modules that are loaded on start up, but the same fate is present for the modules loaded on demand.
The App/PrismApplication class in full:
public partial class App : PrismApplication
{
private ILoggerFactory loggerFactory;
private TaskbarIcon _taskbarIcon;
protected override Window CreateShell()
{
// Register an instance of the Window (used by the dialog service to get a handle on the window when displaying dialogs)
Container.GetContainer().RegisterInstance(typeof(Window), "MainWindow", Container.Resolve<MainWindow>(), new ContainerControlledLifetimeManager());
return Container.Resolve<MainWindow>();
}
protected override void OnInitialized()
{
base.OnInitialized();
// Create the links to each module for the banner view to display
var menuService = Container.Resolve<IMenuService>();
var regionManager = Container.Resolve<IRegionManager>();
menuService.AddItem("Home", "Home", () => regionManager.RequestNavigate("MainRegion", "HomeMainView"));
menuService.AddItem("Messaging", "Messaging", () => regionManager.RequestNavigate("MainRegion", "MessagingMainView"));
menuService.AddItem("Charts", "Charts", () => regionManager.RequestNavigate("MainRegion", "ChartsMainView"));
menuService.AddItem("Admin", "Admin", () => regionManager.RequestNavigate("MainRegion", "AdminMainView"));
// Register banner view with region manager and display it
regionManager.RegisterViewWithRegion("BannerRegion", typeof(BannerView));
// Load the desired module into the main window on start up
Container.Resolve<IRegionManager>().RequestNavigate("MainRegion", "HomeMainView");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
var container = this.ConfigureLogging(containerRegistry);
// Register types
container.RegisterInstance<IDbConnectionFactory>(
new SqlConnectionFactory(
ConfigurationManager.ConnectionStrings["Messaging"].ConnectionString,
loggerFactory));
/************************************************************/
// TODO: Not sure if need singletons or not - to test......
/************************************************************/
containerRegistry.RegisterSingleton<IMetroMessageDisplayService, MetroMessageDisplayService>();
containerRegistry.RegisterSingleton<IUserService, UserService>();
containerRegistry.RegisterSingleton<IUserStore, UserStore>();
containerRegistry.RegisterSingleton<IMenuService, MenuService>();
containerRegistry.Register<ICustomerService, CustomerService>();
containerRegistry.Register<INotifyIconService, NotifyIconService>();
containerRegistry.RegisterDialog<DefaultDialog, DefaultDialogViewModel>("Default");
containerRegistry.RegisterDialog<HtmlDialog, HtmlDialogViewModel>("Html");
// Get the current user's details - prevent a deadlock in the way we use Task.Run(...).Result
// Then add to container as we will be injecting into VMs
var clientUser = Task.Run(GetCurrentUserDetails).Result;
containerRegistry.RegisterInstance(clientUser);
containerRegistry.RegisterSingleton<IClientUser, ClientUser>();
// Add the task bar icon
_taskbarIcon = (TaskbarIcon)FindResource("NotifyIcon");
containerRegistry.RegisterInstance(_taskbarIcon);
// Create a logger instance
var logger = loggerFactory.CreateLogger<App>();
logger.LogDebug("Finished registering types in App.xaml.cs");
}
private async Task<ClientUser> GetCurrentUserDetails()
{
var userService = Container.Resolve<IUserService>();
var data = await userService.GetClientUsersAsync(ConfigurationManager.AppSettings.Get("TempUserName")).ConfigureAwait(true);
if (!data.Any())
{
// log unable to load user from database then return as no point in loading messages for a user that cannot be found!!
return null;
}
return data.FirstOrDefault();
}
protected override IModuleCatalog CreateModuleCatalog()
{
// We are returning a type that reads the modules from the config file.
return new ConfigurationModuleCatalog();
}
protected override void OnExit(ExitEventArgs e)
{
// The icon would clean up automatically, but this is cleaner
_taskbarIcon.Dispose();
base.OnExit(e);
}
private IUnityContainer ConfigureLogging(IContainerRegistry containerRegistry)
{
// Configure logging - Needed Unity.Microsoft.Logging package
// see https://github.com/unitycontainer/microsoft-logging and https://github.com/unitycontainer/examples/tree/master/src/Logging/Microsoft.Logging
var serilogLogger = new LoggerConfiguration()
.ReadFrom.AppSettings()
.CreateLogger();
this.loggerFactory = new LoggerFactory().AddSerilog(serilogLogger);
var container = containerRegistry.GetContainer();
container.AddExtension(new LoggingExtension(loggerFactory));
return container;
}
}
The messaging module:
public class MessagingModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
// Register main view with region manager
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(MessagingMainView));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IMessageStore, MessageStore>();
containerRegistry.RegisterSingleton<IMessageService, MessageService>();
containerRegistry.RegisterSingleton<IChatService, ChatService>();
containerRegistry.RegisterSingleton<IFileDialogService, FileDialogService>();
containerRegistry.RegisterDialog<InboxMessageView, InboxMessageViewModel>("HtmlMessage");
// Here we are loading the config/settings from the app.config for this module so we
// can register a ChatServiceConfiguration type as it is injected into ChatService
var filename = Assembly.GetExecutingAssembly().Location;
var configuration = ConfigurationManager.OpenExeConfiguration(filename);
if (configuration != null)
{
var hubUrl = configuration.AppSettings.Settings["HubUrl"].Value;
if (string.IsNullOrEmpty(hubUrl))
{
throw new ArgumentException("The HubUrl app setting cannot ne null or whitespace.");
}
containerRegistry.RegisterInstance(new ChatServiceConfiguration(hubUrl));
containerRegistry.RegisterSingleton<IChatServiceConfiguration, ChatServiceConfiguration>();
}
}
}
The MessagingMainView:
<UserControl x:Class="Ascensos.Wpf.Modules.Messaging.Views.MessagingMainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:materialDesignConverters="clr-namespace:MaterialDesignThemes.Wpf.Converters;assembly=MaterialDesignThemes.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:Ascensos.Wpf.Modules.Messaging.Views"
xmlns:helpers="clr-namespace:Ascensos.Wpf.Modules.Messaging.Helpers"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
</UserControl>
The code behind:
public sealed partial class MessagingMainView : UserControl
{
public MessagingMainView()
{
this.InitializeComponent();
}
}
The viewmodel (calls a base message view model class):
public sealed class MessagingMainViewModel
{
public MessagingMainViewModel()
{
}
}
UPDATE: I have removed code from my app, so that only the above code is within the view and viewmodel. The view constructor and viewmodel constructor is still being hit twice during the module initialization.
I do not explicitly create instances of views/viewmodels in the code behind anywhere (i.e. only in XAML).
This is a contradiction - creating a view model in xaml means creating one in the same way as in code behind or anywhere else.
As said before, just don't do it. Remove all references to your view models' constructors from xaml, too (like <DataContext><MyViewViewModel/></DataContext>). To get intellisense in xaml, use d:DataContext.
I have found the issue. It is the following line in App CreateShell() method:
Container.GetContainer().RegisterInstance(typeof(Window), "MainWindow", Container.Resolve<MainWindow>(), new ContainerControlledLifetimeManager());
Commenting this out has sorted the problem. Looking at it again I have been rather silly to not spot this, like the novice I am. This code was added so I could get access to the MainWindow/MetroWindow in a service class - but I will need to try this another way. Thanks for your time #Haukinger.
Related
I am trying to populate the ItemsSource of a ComboBox (a derivative of ItemsControl) via a region.
View
The scoped RegionManager (found on the View Model) is assigned to the view via prism:RegionManager.RegionManager="{Binding RegionManager}".
MainWindow.xaml
<Window x:Class="Applications.Testing.Wpf.RegionCreationTester.MainWindow"
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:Applications.Testing.Wpf.RegionCreationTester"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
prism:RegionManager.RegionManager="{Binding RegionManager}"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<ComboBox prism:RegionManager.RegionName="{x:Static local:RegionNames.itemsControlRegion}"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace Applications.Testing.Wpf.RegionCreationTester
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, IModelled<MainWindowViewModel>
{
public MainWindowViewModel ViewModel
{
get
{
return (MainWindowViewModel)DataContext;
}
set
{
DataContext = value;
}
}
public MainWindow()
{
InitializeComponent();
ViewModel.PopulateItemsControl();
}
}
}
ViewModel
The view model is assigned to the view's DataContext via prism:ViewModelLocator.AutoWireViewModel="True".
MainWindowViewModel.cs
namespace Applications.Testing.Wpf.RegionCreationTester
{
public class MainWindowViewModel
{
/// <summary>
/// Gets the <see cref="RegionManager"/> scoped to this control.
/// </summary>
/// <remarks>Exists so that child controls can register regions for their own child controls which are also child controls in this control.</remarks>
public RegionManager RegionManager { get; } = new RegionManager();
/// <summary>
/// Adds some child views to the <see cref="RegionNames.itemsControlRegion"/>.
/// </summary>
/// <remarks>Normally these views would be resolved using an IoC container but this have been omitted for brevity.</remarks>
public void PopulateItemsControl()
{
var region = RegionManager.Regions[RegionNames.itemsControlRegion];
region.Add(new TextBlock { Text = "Item #1" });
region.Add(new Button { Content = "Item #2" });
}
}
}
RegionNames.cs
namespace Applications.Testing.Wpf.RegionCreationTester
{
public static class RegionNames
{
public const string itemsControlRegion = "collectionRegion";
}
}
Bootstrapper
App.xaml
<Application x:Class="Applications.Testing.Wpf.RegionCreationTester.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"/>
App.xaml.cs
namespace Applications.Testing.Wpf.RegionCreationTester
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new RegionCreationTesterBootstrapper().Run();
}
}
}
RegionCreationTesterBootstrapper.cs
namespace Applications.Testing.Wpf.RegionCreationTester
{
public class RegionCreationTesterBootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
=> new MainWindow();
protected override void InitializeShell()
{
base.InitializeShell();
(Application.Current.MainWindow = (Window)Shell).Show();
}
}
}
Once all the region population has occurred and the application is about to run I get a Prism.Regions.UpdateRegionsException containing an InnerException with the message "Region with the given name is already registered: collectionRegion" on the line in App of new RegionCreationTesterBootstrapper().Run(). The last line in my code I am able to get a breakpoint hit for is the new MainWindow() in CreateShell after the call to the constructor for MainWindow has exited. Why am I being told the region is already registered when I am only trying to register it once? I have set breakpoints in the MainWindow's constructor to indeed confirm that it only being created once and even if it weren't, the RegionManager to which it is scoped should prevent this exception from occurring. What have I missed?
UPDATE
I have just commented out the code within PopulateItemsControl and found that the exception is thrown even if only one view is added to the region and stranger still, if no views are added to the region but the region is accessed (as done in the line: var region = RegionManager.Regions[RegionNames.itemsControlRegion];). Therefore the issue is now to do with accessing an existing region on a scoped RegionManager for View Injection in order to add views to it; I'm not sure why accessing a region from the RegionManager would change its state, this seems like a bug in Prism or perhaps something to do with lazy enumeration.
Well, you are doing it twice, you're just not aware. When you set RegionName attached property on your ComboBox, an event hanler that will create the region with given name is attached (that's static part of RegionManager). When the instance of RegionManager you instantiated in your VM tries to access the region collection, indexer first calls a static method on RegionManager class that raises the event. The global RegionManager instance that got the task of creating the region (when you used RegionName attached property) has not finished it's job - window hasn't been loaded when you try to access the region with your instance, the handler has not been removed and it's called again. If you called your PopulateItemsControl method after the window has loaded (say in MainWindow's Loaded event handler), you wouldn't get the exception, but your code would not work as you expect. That is because your instance of RegionManager is not "handling" your collectionRegion, global RegionManager is.
Injecting RegionManager instance
If you need a RegionManager instance in your VM, use constructor injection.
public class MainWindowViewModel : BindableBase
{
private IRegionManager rm;
public MainWindowViewModel(IRegionManager manager)
{
this.rm = manager;
}
public void PopulateItemsControl()
{
var region = rm.Regions[RegionNames.itemsControlRegion];
region.Add(new TextBlock { Text = "Item #1" });
}
}
Dependency injection container (Unity or whatever you're using) will resolve IRegionManager instance when creating the VM (PRISM is doing that job for you anyway, you're not instantiating it yourself).
Region scopes
RegionManager keeps a collection of regions and does not allow regions with same names. So, unless your window is going to have multiple of those ComboBoxes that all have a region named collectionRegion, the RegionManager (the global one) is fine. If your collectionRegion is going to have instances of same view class, that all define another region within itself, then you need region scopes - RegionManager instances with their own scope for those views. In that case, the Add method can create local instance of RegionManager for that view:
IRegion collectionRegion = this.regionManager.Regions["collectionRegion"];
bool makeRegionManagerScope = true;
IRegionManager localRegionManager =
collectionRegion.Add(view, null, makeRegionManagerScope);
This really should be easy, but I could not establish it.
I have small WPF application with Prism 6
I have Main Window and two views inside it.
MainWindow with MainWindowViewModel view model class
ConfigurationView with ConfigurationViewModel view model class
SignInView with SignInViewModel view model class
Now when show the main window for the first time, I want to select which view to show according to some boolean condition
here is the snippet of the bootstrap class.
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
ViewModelLocationProvider.Register<SignInView, SignInViewModel>();
ViewModelLocationProvider.Register<ConfigurationView, ConfigurationViewModel>();
Container.RegisterInstance(new SignInView());
Container.RegisterInstance(new ConfigurationView());
}
protected override DependencyObject CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void InitializeShell()
{
Application.Current.MainWindow.Show();
IRegionManager _regionManager = Container.Resolve<IRegionManager>();
IRegion _region = _regionManager.Regions[RegionNames.MainRegion];
_region.Add(Container.Resolve<SignInView>());
_region.Add(Container.Resolve<ConfigurationView>());
}
Right now always the SignInView is displayed when the main window of the application is opened?
How can I select which view to show according to some condition which need to be brought from the MainWindowViewModel class ?
Update
public class MainWindowViewModel
{
private IAccountService _accountService;
public MainWindowViewModel(IUnityContainer container)
{
IRegionManager regionManager = Container.Resolve<IRegionManager>();
_accountService = container.Resolve<IAccountService>();
if (_accountService.IsSignedIn)
regionManager.RequestNavigate(RegionNames.MainRegion, new Uri(nameof(ConfigurationView), UriKind.Relative));
else
regionManager.RequestNavigate(RegionNames.MainRegion, new Uri(nameof(SignInView), UriKind.Relative));
}
}
Main Window View
<Window x:Class="Shell.Views.MainWindow"
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:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
this code is not working.
The region needs to be created before you can add a view to it. So you'd better create the MainViewModel yourself after the MainWindow has been created:
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.Register<SignInView, SignInViewModel>();
ViewModelLocationProvider.Register<ConfigurationView, ConfigurationViewModel>();
Container.RegisterInstance(new SignInView());
Container.RegisterInstance(new ConfigurationView());
}
protected override DependencyObject CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void InitializeShell()
{
IRegionManager _regionManager = Container.Resolve<IRegionManager>();
IRegion _region = _regionManager.Regions[RegionNames.MainRegion];
_region.Add(Container.Resolve<SignInView>());
_region.Add(Container.Resolve<ConfigurationView>());
var mainWindowViewModel = Container.Resolve<MainWindowViewModel>();
Application.Current.MainWindow.DataContext = mainWindowViewModel;
Application.Current.MainWindow.Show();
}
Remove this from the MainWindow:
prism:ViewModelLocator.AutoWireViewModel="True">
First of all, register the types of your views for navigation, not view instances.
Secondly: conditions - or more general: data - rarely originates from a view model (unless it's user input), so you should have some IUserManagement service to pull the current user from.
But that being said, if you have your view registered, you should be able to navigate to it. Without a concrete exception, it's difficult to guess what's going wrong.
Container.RegisterTypeForNavigation<SignInView>();
...
_regionManager.RequestNavigate( RegionNames.MainRegion, typeof(SignInView).Name );
I have a WPF application where I am using multiple forms. There is one main form which gets opened when we start the application which is know as MainWindow.xaml. This form then have multiple forms which gets opened depending on the user option. There is a form StartClassWindow.xaml. Currently I am working on this form so I want it to start directly instead of MainWindow.xaml. So to do this I changed the app.xaml startupuri:
<Application x:Class="Class.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DispatcherUnhandledException="Application_DispatcherUnhandledException"
StartupUri="StartClassWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
But then it started giving error like below:
No matching constructor found on type 'Class.StartClassWindow'. You
can use the Arguments or FactoryMethod directives to construct this
type.' Line number '3' and line position '9'.
Here is the StartClassWindow.xaml.cs:
namespace Class
{
public partial class StartClassWindow : System.Windows.Window
{
public StartClassWindow(string classData)
{
InitializeComponent();
className = classData;
function();
}
//rest of the code.
}
}
You need to add a parameter-less constructor to your StartClassWindow like this:
public StartClassWindow(string classData)
{
InitializeComponent();
className = classData;
function();
}
public StartClassWindow()
{
}
Or if you don't want to have another constructor you can override the OnStartup method in the App.xaml.cs but you should remove the StartupUri="StartClassWindow.xaml" in your App.xaml first. Like below:
protected override void OnStartup(StartupEventArgs e)
{
StartClassWindow st = new StartClassWindow("");
st.Show();
}
"Normally", your constructor must be parameterless:
public Login()
But, since you are using dependency injection, like this:
public Login(IUserService userService)
The constructor isn't parameterless, and the framework cannot instantiate the page if it's not told how to.
For this there are a couple of options:
Remove service from constructor
Just like this, but you'll need to access the userservice differently:
public Login()
pre .net 4.8, 4.7, using Unity (or Prism)
You can use a dependency injection framework like Unity to register the components.
It is described here:
https://www.wpftutorial.net/ReferenceArchitecture.html
public class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IUnityContainer container = new UnityContainer();
container.RegisterType<IUserService, UserService>();
MainWindow mainWindow = container.Resolve<MainWindow>();
mainWindow.Show();
}
}
Manual using Navigation Service
Manually navigate, and do your own construction:
NavigationService.Navigate(new LoginPage(new UserService);
As described here: https://learn.microsoft.com/en-us/dotnet/desktop/wpf/app-development/navigation-overview?view=netframeworkdesktop-4.8
.net 5 WPF, using built in DI
If you are using .net 5, here is a tutorial. Make sure to register both the window and the service:
https://executecommands.com/dependency-injection-in-wpf-net-core-csharp/
Here's an example:
private void ConfigureServices(ServiceCollection services)
{
services.AddScoped<IUserService,UserService>();
services.AddSingleton<MainWindow>();
}
private void OnStartup(object sender, StartupEventArgs e)
{
var mainWindow = serviceProvider.GetService<MainWindow>();
mainWindow.Show();
}
I'm trying to open a settings view in a Caliburn.Micro WinRT 8.1 app using VS2013 RC, but I keep getting an unhandled exception when opening it with the following message:
Value cannot be null. Parameter name: Could not parse the VisualElements from the app manifest.
I can reproduce the issues with the following steps:
create a new Windows Store app from VS2013 RC using the Blank app template.
add Caliburn.Micro via NuGet.
in App.xaml, change the base class to caliburn:CaliburnApplication (the namespace is declared as xmlns:caliburn="using:Caliburn.Micro").
in App.xaml.cs, change the class like this (for the CM-based settings I follow http://compiledexperience.com/blog/posts/settings-caliburn)
Code below:
public sealed partial class App
{
private WinRTContainer _container;
public App()
{
InitializeComponent();
}
protected override void Configure()
{
_container = new WinRTContainer();
_container.RegisterWinRTServices();
_container.PerRequest<MainViewModel>();
_container.PerRequest<SettingsViewModel>();
ISettingsService settings = _container.RegisterSettingsService();
settings.RegisterCommand<SettingsViewModel>("Test settings");
}
protected override object GetInstance(Type service, string key)
{
var instance = _container.GetInstance(service, key);
if (instance != null) return instance;
throw new Exception("Could not locate any instances.");
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}
protected override void PrepareViewFirst(Frame rootFrame)
{
_container.RegisterNavigationService(rootFrame);
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
DisplayRootView<MainView>();
}
}
Finally, create folders for Views and ViewModels in the solution add add to them the required items: MainViewModel, SettingsViewModel, MainView, SettingsView. The views just include a TextBlock with some text. MainViewModel derives from Screen, while SettingsViewModel derives from PropertyChangedBase. There is no relevant code in any of them.
When launching the app, I can see the main view; then I open the charms bar and click settings, and I find the label leading to my app settings; when I click it, I get the exception quoted above. Any hint?
You can find a full repro solution here: http://sdrv.ms/18GIMvB .
If you aren't ready to move to the alpha version of CM, you can update Callisto to 1.4.0 via NuGet. That fixed the error for me.
It seems that the new CM release (alpha 2) fixed the issue, so I'm adding some more information here to help other newcomers like me. Here is what I'm doing now:
In app's Configure I have some bootstrap code like:
...
ResourceLoader loader = ResourceLoader.GetForViewIndependentUse("Resources");
ISettingsService settings = _container.RegisterSettingsService();
settings.RegisterFlyoutCommand<ContentSettingsViewModel>(loader.GetString("SettingsContent"));
The ContentSettingsViewModel is a viewmodel for filtering some contents. The string got from resources is the label which will appear in the settings flyout (be sure there is an entry for this string, as passing an empty or null string triggers an exception). This VM is derived from CM Screen as I'm overriding OnActivate and OnDeactivate to load and save settings when the user opens or dismisses the settings page.
Have been learning about MVP and have tried writing a test app using it in WinForms. I'm struggling to find a well explained example on how to navigate between my forms/views. As an example, the program starts and I want to show a login dialog then go into my main view if the login was successful. At the moment, my Main method looks something like this:
static void Main()
{
var loginView = Injector.Resolve<ILoginView>();
if (loginView.DoLogin() != LoginResult.OK) return;
var mainView = Injector.Resolve<IMainView>();
Application.Run(mainView); // won't work as mainView isn't a form
}
The Injector object is just a wrapper around an IoC tool (currently StructureMap). The thing is, I've read that I shouldn't really be manually creating instances via the Injector as they should really be done via constructor injection.
I've managed to do this up to a point but not when it comes to navigation. I can't think of an elegant way of moving through my views and was wondering if anyone here might shed some light on this? I've read a little on application controllers but have not found an example to show it clearly.
In regards to the navigation question:
I've managed to do this up to a point but not when it comes to
navigation. I can't think of an elegant way of moving through my views
and was wondering if anyone here might shed some light on this? I've
read a little on application controllers but have not found an example
to show it clearly.
Below is a simplified version of a construct I've used. Note that the setup and tear down hooks are called automatically when the NavigateTo method is called. Also, +1 to #AlexBurtsev, as his answer hints at this very same approach.
// Presenter can and should offer common services for the
// subclasses
abstract class Presenter
{
public void Display()
{
OnDisplay();
}
public void Dismiss()
{
OnDismiss();
}
protected virtual OnDisplay() // hook for subclass
{
}
protected virtual OnDismiss() // hook for subclass
{
}
private NavigationManager _navMgr;
internal NavigationMgr NavigationManager
{
get
{
return _navMgr;
}
set
{
_navMgr = value;
}
}
}
// NavigationManager is used to transition (or navigate)
// between views
class NavigationManager
{
Presenter _current;
// use this override if your Presenter are non-persistent (transient)
public void NavigateTo(Type nextPresenterType, object args)
{
Presenter nextPresenter = Activator.CreateInstance(nextPresenterType);
NavigateTo(nextPresenter);
}
// use this override if your Presenter are persistent (long-lived)
public void NavigateTo(Presenter nextPresenter, object args)
{
if (_current != null)
{
_current.Dismiss()
_current.NavigationMgr = null;
_current = null;
}
if (nextPresenter != null)
{
_current = nextPresenter;
_current.NavigationMgr = this;
_current.Display(args);
}
}
}
class MainMenuPresenter : Presenter
{
private IMainMenuView _mainMenuView = null;
// OnDisplay is your startup hook
protected override void OnDisplay()
{
// get your view from where ever (injection, etc)
_mainMenuView = GetView();
// configure your view
_mainMenuView.Title = GetMainTitleInCurrentLanguage();
// etc
// etc
// listen for relevent events from the view
_mainMenuView.NewWorkOrderSelected += new EventHandler(MainMenuView_NewWorkOrderSelected);
// display to the user
_mainMenuView.Show();
}
protected override void OnDismiss()
{
// cleanup
_mainMenuView.NewWorkOrderSelected -= new EventHandler(MainMenuView_NewWorkOrderSelected);
_mainMenuView.Close();
_mainMenuView = null;
}
// respond to the various view events
private void MainMenuView_NewWorkOrderSelected(object src, EventArgs e)
{
// example of transitioning to a new view here...
NavigationMgr.NavigateTo(NewWorkOrderPresenter, null);
}
}
class NewWorkOrderPresenter : Presenter
{
protected override void OnDisplay()
{
// get the view, configure it, listen for its events, and show it
}
protected override void OnDismiss()
{
// unlisten for events and release the view
}
}
I haven't used WinForms for a long time, but I'll try to answer this. I would use the same strategy as WPF Prism do.
About MainView and Application.Run:
Create a main Region (root Form), with empty container inside which can hold UserControl (I forgot exact class names), then when you need to switch root view, you do RootView.SetView(UserControl view) which will do something like Form.Clear(), Form.AddChild(view).
About the navigation and using container:
You could create a service for navigation: INavigationService which you inject in constructors with method like INavigationService.NavigateView(String(or Type) viewName, params object[] additionalData)
You can insert a method in mainView that returns the actual form.Then you can call
Mainview:IMainView
{
Form GetView()
{
//return new Form();
}
}
In Main you can call ,
Application.Run(mainView.GetView())