Caliburn Micro content control navigation - wpf

I'm using caliburn micro for this project.
I have my ShellView with my contentcontrol:
<ContentControl x:Name="ActiveItem"
Grid.Row="0" Grid.Column="0" />
In ShellViewModel i got it to show my usercontrol LoginView with:
public class ShellViewModel : Conductor<object>
{
public ShellViewModel()
{
ActivateItem(new LoginViewModel());
}
public void ShowSignUp()
{
ActivateItem(new SignUpViewModel());
}
}
However, i can't navigate to SignUpView from LoginView with my button:
<!-- Row 4 -->
<Button x:Name="ShowSignUp"
Content="Sign Up Now!"
Grid.Row="3" Grid.Column="1"
Style="{StaticResource LoginBtnsStyle}" />
LoginViewModel deriving from ShellViewModel:
public class LoginViewModel : ShellViewModel
{
}
How do i navigate from LoginView to SignUpView with a button that is on the LoginView?
I'm getting no errors, it just isn't changing view.
I also tried putting ShowSignUp() on the LoginViewModel but no success.
Update 1 ShellViewModel:
public class ShellViewModel : Conductor<object>, IHandle<ActionInvokedMessage>
{
DispatcherTimer dt = new DispatcherTimer();
private SplashScreenViewModel _splashVM;
private LoginViewModel _loginVM;
private SignUpViewModel _signUpVM;
private IEventAggregator _eventAggregator;
public ShellViewModel(SplashScreenViewModel splashVM, LoginViewModel loginVM, SignUpViewModel signUpVM)
{
_loginVM = loginVM;
_signUpVM = signUpVM;
_splashVM = splashVM;
ActivateItem(_splashVM);
dt.Tick += new EventHandler(Dt_Tick);
dt.Interval = new TimeSpan(0, 0, 2);
dt.Start();
}
private void Dt_Tick(object sender, EventArgs e)
{
dt.Stop();
ActivateItem(_loginVM);
}
public ShellViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
ActivateItem(new LoginViewModel(_eventAggregator));
}
public void Handle(ActionInvokedMessage message)
{
ActivateItem(message.Page);
}
public void ShowSignUp()
{
ActivateItem(new SignUpViewModel());
}
}

You could achieve this using EventAggregator to publish indicative messages from LoginViewModel to ShellViewModel to update the UI.
To begin with, you need to define an message class, which would tells the ShellViewModel which ViewModel needs to be changed. For example,
public class ActionInvokedMessage
{
public Screen Page { get; set; }
}
The Page property would indicate which Screen needs to be loaded. Now, you could change your LoginViewModel as the following.
public class LoginViewModel: Screen
{
private IEventAggregator _eventAggregator;
public LoginViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
}
public void ShowSignUp()
{
_eventAggregator.PublishOnUIThread(new ActionInvokedMessage { Page = new SignupViewModel() }); ;
}
}
The PublishOnUIThread method would broadcast a message to all the listeners of the Message Type ActionInvokedMessage for the change. Next step would be to ensure the ShellViewModel would be listening to the change.
public class ShellViewModel : Conductor<object>, IHandle<ActionInvokedMessage>
{
private IEventAggregator _eventAggregator;
public ShellViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
ActivateItem(new LoginViewModel(_eventAggregator));
}
public void Handle(ActionInvokedMessage message)
{
ActivateItem(message.Page);
}
public void ShowSignUp()
{
ActivateItem(new SignupViewModel());
}
}
The Implementation of IHandle interface allows us to handle the action that would be required to be executed when the ShellViewModel recieves the ActionInvokedMessage. As seen in the code, this would be an appropriate place to use the ActivateItem method to load the Signup Page.

You can create an interface for navigation and use it in view models to navigate around app.
interface INavigation {
void NavigateTo(System.Type typeId);
}
class ShellViewModel: Conductor<object>, INavigation {
private List<object> pages = new List<Object>();
public ShellViewModel() {
pages.Add(new SignupViewModel(this));
pages.Add(new LoginViewModel(this));
}
void NavigateTo(System.Type typeId) {
var page = pages.Where(x => x.GetType() == typeId).FirstOrDefault()
ActivateItem(page);
}
}
class SignupViewModel {
public SignupViewModel(INavigation navigation) {
this.ShowLoginCommand= new NavigateCommand<LoginViewModel>(navigation);
}
}
class LoginViewModel {
public LoginViewModel (INavigation navigation) {
this.ShowSignUpCommand = new NavigateCommand<SignupViewModel>(navigation);
}
}
Navigation command may be implemented like follows:
public class NavigateCommand<T> : ICommand
{
public event EventHandler CanExecuteChanged;
private readonly INavigation navigation;
public NavigateCommand(INavigation navigation)
{
this.navigation = navigation;
}
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => this.navigation.NavigateTo(typeof(T));
}
Here I pass System.Type but you can design type that better describes navigation request so that you may pass additinal paramters.

Related

Navigation between lazy loaded modules with Prism and WPF

Hello I'm trying to setup an architecture where only one module gets booted when the app is launched. Then I'd like to lazy load other modules based on the user's actions.
To achieve this in my app.xaml.cs I have one module loaded at bootstrap time (MainModule), and an other has InitializationMode = InitializationMode.OnDemand
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
Type BlipModuleType = typeof(BlipModule);
moduleCatalog.AddModule(new ModuleInfo()
{
ModuleName = BlipModuleType.Name,
ModuleType = BlipModuleType.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand
});
moduleCatalog.AddModule<MainModule>();
}
then my main module, which displays the view correctly, has a single view registered to the only region available:
public class MainModule : IModule
{
private readonly IRegionManager _regionManager;
public MainModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void OnInitialized(IContainerProvider containerProvider)
{
_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(ViewA));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
The lazy loaded module has the same structure, registering a different view (which works properly if i decide to use it as my main module)
public class BlipModule : IModule
{
private readonly IRegionManager _regionManager;
public BlipModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void OnInitialized(IContainerProvider containerProvider)
{
_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(ViewB));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
finally I have a Command in the viewmodel of my MainModule ViewA, that is supposed to load the new module and navigate to it.
public class ViewAViewModel : BindableBase
{
const string BlipModuleName = "BlipModule";
public ReactiveCommand ChangeRoute { get; set; } = new ReactiveCommand();
public ViewAViewModel(IRegionManager regionManager, IModuleManager moduleManager)
{
ChangeRoute.Subscribe(res =>
{
moduleManager.LoadModule(BlipModuleName);
});
moduleManager.LoadModuleCompleted += (s, e) =>
{
if (e.ModuleInfo.ModuleName == BlipModuleName)
{
regionManager.RequestNavigate(RegionNames.ContentRegion, new Uri(BlipModuleName, UriKind.Relative));
}
};
}
}
The viewB of the BlipModule is actually loaded (I get a hit if I set a breakpoint in the view's constructor), but instead of the view I get a white page with "System.Object" inside of it.
Any idea? thanks!
You want to RegisterForNavigation instead of RegisterViewWithRegion.

MEF, Prism and new view instance on navigation

The code below worked on shared instance of the view. Now what I'm trying to achieve is each time I navigate to ViewB I want a new instance of the view and its backing view model. I have tried various combinations of the below but they all seem to ultimately end with the RequestNavigate failing silently the second time I try to navigate to ViewB...
I have also tried setting IsNaviagtionTarget to false after the view has been navigated to once.
Bootstrapper:
public void Initialize()
{
_regionManager.RegisterViewWithRegion(RegionNameConstants.MainRegion, typeof(ViewA));
_regionManager.RegisterViewWithRegion(RegionNameConstants.MainRegion, typeof(ViewB));
}
ViewB (class):
[RegionMemberLifetime(KeepAlive = false)]
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
internal partial class ViewB
{
[ImportingConstructor]
public ViewB(ViewBViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
ViewBViewModel:
[Export(typeof(ViewBViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
internal class ViewBViewModel : BindableBase, INavigationAware
{
public void OnNavigatedTo(NavigationContext navigationContext)
{
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
}
ViewA simply has a button with a command that calls:
ViewA Navigation command:
public override void Execute(object parameter)
{
_regionManager.RequestNavigate(RegionNameConstants.MainRegion, new Uri(nameof(ViewB), UriKind.Relative));
}
Don't register typeof(ViewB) with the region manager in the bootstrapper:
public void Initialize()
{
_regionManager.RegisterViewWithRegion(RegionNameConstants.MainRegion, typeof(ViewA));
}
And since you are navigating to the Uri of nameof(ViewB), you should also export the view with a contract name of nameof(ViewB):
[Export(nameof(ViewB))]
[PartCreationPolicy(CreationPolicy.NonShared)]
[RegionMemberLifetime(KeepAlive = false)]
internal partial class ViewB
{
[ImportingConstructor]
public ViewB(ViewBViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
Then you should get a new instance of ViewB each time you navigate to it using:
_regionManager.RequestNavigate(RegionNameConstants.MainRegion, new Uri(nameof(ViewB), UriKind.Relative));

Update viewmodel based on MainWindow event

I have a UdpClient, firing off a DataRecevied event on my MainWindow:
public partial class MainWindow : Window
{
public static YakUdpClient ClientConnection = new YakUdpClient();
public ClientData;
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
ClientData = new ClientData();
ClientConnection.OnDataReceived += ClientConnectionOnDataReceived;
}
private void ClientConnectionOnDataReceived(object sender, MessageEventArgs messageEventArgs)
{
ClientData.Users = messageEvenArgs.ConnectedUsers;
}
}
My ClientData and User classes look as follow:
public class ClientData
{
public List<User> Users {get;set;)
}
public class User
{
public string Name {get;set;}
}
On my MainWindow, I have a UserControl called UserListView which has a ViewModel called UserListViewModel
The ViewModel looks as follow:
public class UserListViewModel: BindableBase
{
public UserListViewModel()
{
//I am sure there are better ways of doing this :(
Users = new ObservableCollection<User>((MainWindow)Application.Current.MainWindow).ClientData.Users
});
private ObservableCollection<User> _users;
public ObservableCollection<User> Users
{
get{ return _users;}
set { this.SetProperty(ref this._users, value); }
}
}
The difficulty I have here, is when the ClientConnectionOnDataReceived event on the MainWindow gets fired, I would like to update my ClientData class, My Viewmodel should then somehow be notified that the list changed, and subsequently update my UI.
Can anyone give me a solid example of how to achieve this using MVVM (Prism) in WPF?
I am new to MVVM, so i am still trying to figure this out.
First of all, there's no obvious reason why the main window should do the subscription.
I'd go for something like this:
create a service that encapsulates the subscription (and subscribes in its constructor)
register that as a singleton
have it implement INotifyPropertyChanged (to notify consumers of a change to Users)
inject the service into UserListViewModel and observe the Users property (see PropertyObserver)
when Users in the service changes, update Users in the user list view model
and best of all, no need for ObservableCollection here :-)
EDIT: example:
interface IUserService : INotifyPropertyChanged
{
IReadOnlyCollection<User> Users
{
get;
}
}
class YakUdpService : BindableBase, IUserService
{
private readonly YakUdpClient _yakUdpClient;
private IReadOnlyCollection<User> _users;
public YakUdpService()
{
_yakUdpClient = new YakUdpClient();
_yakUdpClient.OnDataReceived += ( s, e ) => Users = e.ConnectedUsers;
}
public IReadOnlyCollection<User> Users
{
get
{
return _users;
}
private set
{
SetProperty( ref _users, value );
}
}
}
class UserListViewModel : BindableBase
{
private IReadOnlyCollection<UserViewModel> _users;
private readonly IUserService _userService;
private readonly PropertyObserver<IUserService> _userServiceObserver;
public UserListViewModel( IUserService userService )
{
_userService = userService;
_userServiceObserver = new PropertyObserver<IUserService>( userService );
_userServiceObserver.RegisterHandler( x => x.Users, () => Users = _userService.Users.Select( x => new UserViewModel( x ) ).ToList() );
// ^^^ should use factory in real code
}
public IReadOnlyCollection<UserViewModel> Users
{
get
{
return _users;
}
private set
{
SetProperty( ref _users, value );
}
}
}
and then register the service
Container.RegisterType<IUserService, YakUdpService>( new ContainerControlledLifetimeManager() );
in your bootstrapper or your module's initialization.

WPF PRISM: IRegionMemberLifetime KeepAlive doesn't get called

I have a need to switch the view being displayed based on a certain condition.
I have implemented the switching logic in the constructor of the ViewModel
I am implementing IRegionMemberLifetime on the View and setting KeepAlive to false so that I always get a new instance of the View and the ViewModel.
But for some reason, when I click on the Navigation Button, my breakpoint at KeepAlive never reaches and I get the MainView instead of the WelcomeView.
Here is the code for your reference:
Navigation Button:
<Controls:SignedButton VerticalAlignment="Top" Width="275" Height="45"
Foreground="#FFFFFF"
LeftSign="<" Text="Back to Accounts"
TextSize="20" ButtonBackground="#666666"
HoverBackground="#0FBDAC" HoverOpacity="1" Margin="0,25,0,0"
Command="{x:Static Infrastructure:ApplicationCommands.NavigateCommand}"
CommandParameter="{x:Type Views:MainView}"/>
View Model:
[RegionMemberLifetime(KeepAlive = false)]
public class MainViewModel : ViewModel, IMainViewModel
{
private readonly IUnityContainer _container;
private readonly IRegionManager _regionManager;
public MainViewModel(IUnityContainer container, IRegionManager regionManager)
{
_container = container;
_regionManager = regionManager;
Accounts = new List<Account>();
if (Accounts.Any()) return;
IRegion region = _regionManager.Regions[Regions.Main];
var views = region.Views;
foreach (var view in views)
{
region.Remove(view);
}
region.Add(_container.Resolve<IWelcomeView>());
}
public IList<Account> Accounts { get; private set; }
}
View Model Base:
public abstract class ViewModel : IViewModel
{
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
View:
[RegionMemberLifetime(KeepAlive = false)]
public partial class MainView : UserControl, IMainView
{
public MainView(IMainViewModel mainViewModel)
{
InitializeComponent();
ViewModel = mainViewModel;
}
public IViewModel ViewModel
{
get { return (IViewModel) DataContext; }
set { DataContext = value; }
}
}
Shell View Model:
public class ShellViewModel : ViewModel, IShellViewModel
{
private readonly IRegionManager _regionManager;
public ShellViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
NavigateCommand = new DelegateCommand<object>(Navigate);
ApplicationCommands.NavigateCommand.RegisterCommand(NavigateCommand);
}
private void Navigate(object navigatePath)
{
if (navigatePath != null)
{
_regionManager.RequestNavigate(Regions.Main, navigatePath.ToString());
}
}
public DelegateCommand<object> NavigateCommand { get; private set; }
}
It would be useful for you to look into the RegionMemberLifetimeBehavior class in the Prism Library (for me, it is at C:\Prism4\PrismLibrary\Desktop\Prism\Regions\Behaviors)
Both the IRegionMemberLifetime interface and the RegionMemberLifetimeAttribute accomplish the same thing and can be defined on either your View or your ViewModel (provided the viewmodel is set to DataContext).
Here's the code that is relevant:
private void OnActiveViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// We only pay attention to items removed from the ActiveViews list.
// Thus, we expect that any ICollectionView implementation would
// always raise a remove and we don't handle any resets
// unless we wanted to start tracking views that used to be active.
if (e.Action != NotifyCollectionChangedAction.Remove) return;
var inactiveViews = e.OldItems;
foreach (var inactiveView in inactiveViews)
{
if (!ShouldKeepAlive(inactiveView))
{
this.Region.Remove(inactiveView);
}
}
}
private static bool ShouldKeepAlive(object inactiveView)
{
IRegionMemberLifetime lifetime = GetItemOrContextLifetime(inactiveView);
if (lifetime != null)
{
return lifetime.KeepAlive;
}
RegionMemberLifetimeAttribute lifetimeAttribute = GetItemOrContextLifetimeAttribute(inactiveView);
if (lifetimeAttribute != null)
{
return lifetimeAttribute.KeepAlive;
}
return true;
}
I was able to even step this code to see how it interacts with my application. To answer your question, if your KeepAlive property is not getting hit, then it is not being removed from the ActiveViews. Also make sure that if you are resolving your view from a container through IoC that you have not registered it as a singleton type (that you get a new instance each time it is resolved). The attribute/interface will only remove it completely from the region, not guarantee you get a fresh instance.

MEF problem with import

I have problem with import shell-view-model to view-model class, I use MEF.
Shell-view-model :
namespace Spirit.ViewModels
{
using Caliburn.Micro;
using System.ComponentModel.Composition;
public interface IShellViewModel
{
void ShowLogOnView();
void ShowMessengerView();
}
[Export(typeof(IShellViewModel))]
public class ShellViewModel : Conductor<IScreen>, IShellViewModel
{
public ShellViewModel()
{
ShowLogOnView();
}
public void ShowLogOnView()
{
ActivateItem(new LogOnViewModel());
}
public void ShowMessengerView()
{
ActivateItem(new LogOnViewModel());
}
}
}
I need this class import in view-model class:
[Export]
public class LogOnViewModel : Screen, IDataErrorInfo
{
[Import]
private IShellViewModel _shellViewModel;
public void LogOn(string nick, string password)
{
IMessengerViewModel vm = IoC.Get<MessengerViewModel>();
_shellViewModel.ShowMessengerView();
}
}
Problem is after initialize is variable _shellViewModel null.
My bootstraper look like this:
public class MefBootStrapper : Bootstrapper<IShellViewModel>
{
}
MY SOLUTION:
I create interface assembly and refer this assembly in external service dll and also in wpf app.
In bootstraper I load this assembly with reflection:
var catalog =
new AggregateCatalog(
AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>());
catalog.Catalogs.Add(
new AssemblyCatalog(string.Format(
CultureInfo.InvariantCulture, "{0}{1}", System.IO.Directory.GetCurrentDirectory(), #"\Pokec_Toolkit.dll")));
_container = new CompositionContainer(catalog);
Than I create conductor class:
public interface IShellViewModel
{
void ShowLogOnView();
void ShowMessengerView();
}
[Export(typeof(IShellViewModel))]
public class ShellViewModel : Conductor<IScreen>, IShellViewModel
{
public ShellViewModel()
{
ShowLogOnView();
}
public void ShowLogOnView()
{
ActivateItem(IoC.Get<LogOnViewModel>());
}
public void ShowMessengerView()
{
ActivateItem(IoC.Get<MessengerViewModel>());
}
}
And in view-model I have this:
[Export]
public class LogOnViewModel : Screen, IDataErrorInfo, ILogOnViewModel
{
[Import]
private IShellViewModel _shellViewModel;
[Import]
private IPokecConnection _pokecConn;
//this method is bind on event click of button
public void LogOn(string nick, string password)
{
//SHOW NEW WIEW
_shellViewModel.ShowMessengerView();
}
}

Resources