How to load module when using events - wpf

I'm very newbie in C#\Prism ecosystem.
I want to connect modules via events, but if just send event:
_eventAggregator.GetEvent<LoginSuccessEvent>().Publish(new LoginSuccessEventArgs(user));
then my event handler is not working.
As I understand it happens, because reciever ViewModel is not created (I checked with break point in debugger).
But if I navigate from event sender:
_regionManager.RequestNavigate(RegionNames.RootRegion, "WorksheetListView");
_regionManager.RequestNavigate(RegionNames.WorksheetDetailsRegion, "WorksheetDetailsView");
_eventAggregator.GetEvent<LoginSuccessEvent>().Publish(new LoginSuccessEventArgs(user));
Then before first RequestNavigate command reciever ViewModel constructor is called.
_regionManager = regionManager;
_model = container.Resolve<WorksheetListModel>();
OpenWorksheetCommand = new DelegateCommand(OpenWorksheet);
Worksheets = _model.WorksheetList;
eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(OnLoginSuccessEvent);
I tried to add ViewModel class registration to reciever module:
_container.RegisterType<WorksheetListViewModel>();
But no luck. I don't want to add this registration to sender, because hard relation is maked.
But I want to have weak relation between modulel and to do navigation from RECIEVER, but not from SENDER. So sender will don't know anything about reciever.
How can I achive this?
Thanks.

It's a bit unclear what you're trying to achieve, but as far as I get it, your problem's that events are there for those instances that are currently alive. It seems you want more of a state, so that view models that are created after the user logged in can check whether a user is logged in and act accordingly.
I suggest you create a service to hold the currently logged in user and keep the event, because they complement each other nicely.
Example:
public interface IUserManager : INotifyPropertyChanged // this is optional if you keep the event
{
// returns null if no user is logged in
string CurrentUserName { get; }
// returns true if user name and password are valid and updates CurrentUserName
bool TryLogin( string userName, string password );
}

Ok. I found a tons of question like ('EventAggregator don't work around modules').
My solution is very easy. I create a instance of my viewmodel, so constructor with event subscribtion is invoked too.
using Microsoft.Practices.Unity;
using Prism.Modularity;
using Prism.Regions;
using Prism.Events;
using Prism.Unity;
using WorksheetListModule.ViewModels;
using WorksheetListModule.Views;
using WorksheetListModule.Models;
namespace WorksheetListModule
{
public class WorksheetListModule : IModule
{
IRegionManager _regionManager;
IUnityContainer _container;
IEventAggregator _eventAggregator;
public WorksheetListModule(RegionManager regionManager, IUnityContainer container, IEventAggregator eventAggregator)
{
_regionManager = regionManager;
_container = container;
_eventAggregator = eventAggregator;
}
public void Initialize()
{
WorksheetListViewModel vm = new WorksheetListViewModel(_regionManager, _container, _eventAggregator);
_container.RegisterInstance<WorksheetListViewModel>(vm);
_container.RegisterType<WorksheetListModel>();
_container.RegisterTypeForNavigation<WorksheetListView>();
_container.RegisterTypeForNavigation<WorksheetDetailsView>();
}
}
}
Key feature here is RegisterInstance function.
So now I can do navigation in event reciever and event sender now don't know any information about reciever internal structure.

Related

How can I initialize a Prism module without View and ViewModel for working with EventAggregator?

I am writing an application using Prism that contains three modules. First one has a view to configure a "Person", second one is a service that generates that "Person" and third one is the visualization of all people. These three modules communicate with EventAggregator system. But I have problems with the messages on the service one.
In this service module I only have the service implementation and the module definition.
This service is a people manager that receives a message from EventAggregator, creates a "Person" with a task and send a message to the third module with this "Person".
Service:
private List<Person> people = new();
public PeopleControllerService(IEventAggregator eventAggregator, ICommonParametersService commonParameters)
{
this._eventAggregator = eventAggregator;
eventAggregator.GetEvent<GeneratePersonEvent>().Subscribe(GeneratePerson);
this._commonParameters = commonParameters;
}
private void GeneratePerson()
{
Person newPerson = new(this._commonParameters.DefaultPersonTask);
this.People.Add(newPerson);
this._eventAggregator.GetEvent<AssignedPersonEvent>().Publish(newPerson);
}
Module definition:
private PeopleControllerService moduleController;
public void OnInitialized(IContainerProvider containerProvider)
{
IEventAggregator eventAggregator = containerProvider.Resolve<IEventAggregator>();
ICommonParametersService commonParametersService = containerProvider.Resolve<ICommonParametersService>();
this.moduleController = new(eventAggregator, commonParametersService);
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
The problem is that when I send the "GeneratePersonEvent" message it never reaches the PeopleControllerService and the "GeneratePerson" method is never executed.
I've tried using a view and a viewModel, programming the service in the viewModel and assigning the view to a dummy and hidden region in the app and I've verified that it works that way.
Modified module definition:
public void OnInitialized(IContainerProvider containerProvider)
{
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RequestNavigate(RegionNames.DummyRegion, "PeopleController");
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<PeopleController>();
}
How can I use the EventAggregator without using a dummy view? Do I have to add something in the "RegisterTypes" method? I've tried with:
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<PeopleControllerService>();
}
but it doesn't work either.
I've checked this post: Can I get EventAggregator Subscribe Message without view, viewmodel in prism?, and there it says that it is possible, but doesn't describe how to implement.
Most of the time you want exactly one instance of a service, and you have to tell the container:
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<PeopleControllerService>();
}
Also, you want your service to implement an interface so that you can pass different implementations to the consumers of your service, the most obvious case is your tests.
You need to actually create the instance of your service, too. Normally, you inject it into some consumer, but if it's completely decoupled and only talks through the event aggregator, you have to create the instance manually:
// in App.xaml.cs
protected override void OnInitialized()
{
Container.Resolve<PeopleControllerService>();
base.OnInitialized();
}
Hint: if the service implements an interface, the application doesn't need to personally know the controller module.

CaliburnMicro IEventAggregator won't work with ChildViews

and sorry if this is a somewhat easy question, but I'm still new to CaliburnMicro and EventAggregator is proving to be the worst thing to learn by a long mile. Anyway, let's go to it. I have an app with a main ShellView and a bunch of ChildViews that display various information, and I need certain parameters to be shared between the Views. It did not take very long to find I need to use EventAggregator, but with Net6 I could not make it work, no chance. I found this app someone else did in Net3.1 and CaliburnMicro where a new window is created, on this second window there is a TextBox and a send button. Whatever you type here gets sended to the Main window. I studied the code and replicated the app succesfully, also with the latest version of Caliburn and Net6.
BUT then I decided to modify the app and instead of having a new window, now I have exactly the same but with a ChildView inside the ShellView, and here is where nothing works.
1st on the Bootstrapper
public class Bootstrapper : BootstrapperBase
{
private SimpleContainer _container;
public Bootstrapper()
{
Initialize();
}
protected override void Configure()
{
_container = new SimpleContainer();
_container.Singleton<IWindowManager, WindowManager>();
_container.Singleton<IEventAggregator, EventAggregator>();
_container.PerRequest<ShellViewModel>();
}
protected override object GetInstance(Type service, string key)
{
var instance = _container.GetInstance(service, key);
if (instance != null)
return instance;
throw new InvalidOperationException("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 OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
As seen, I've created a SimpleContainer, then initialised it as a Singleton.
Also _container.Singleton<IWindowManager, WindowManager>(); is no longer needed as I am not opening a new window anymore, so that could be commented but as I had so many issues, I let it be just in case for this last try before posting this question.
2nd I've created a class for the message IHandle to manage.
public class EventMessage
{
public string Text { get; set; }
}
3rd I created and edited the ChildViewModel (still called SecondWindowViewModel as this experiment is directly derivated from the original worknig one) and ChildView (well SecondWindowView still). Note that SecondWindowView is a WPF User Control, not a Window.
class SecondWindowViewModel : Screen
{
private readonly IEventAggregator _eventAggregator;
private string _secondTextBox;
public string SecondTextBox
{
get { return _secondTextBox; }
set { _secondTextBox = value; NotifyOfPropertyChange(() => SecondTextBox); }
}
public SecondWindowViewModel(IEventAggregator eventAggregator)
{
this._eventAggregator= eventAggregator;
}
public void SendBack()
{
EventMessage ToSend = new EventMessage();
ToSend.Text = SecondTextBox;
_eventAggregator.PublishOnUIThreadAsync(ToSend);
}
As seen, I have an IEventAggregator _eventAggregator, then on the constructor of the class I added this._eventAggregator= eventAggregator; and then on the method SendBack that is called upon pressing the button I send the message with SubscribeOnUIThread.
And lastly the ShellViewModel:
public class ShellViewModel : Conductor<Object>, IHandle<EventMessage>
{
private readonly IEventAggregator _eventAggregator;
private string _parentText;
public string ParentText
{
get { return _parentText; }
set { _parentText = value; NotifyOfPropertyChange(() => ParentText); }
}
public ShellViewModel(IEventAggregator eventAggregator)
{
ActivateItemAsync(new SecondWindowViewModel(_eventAggregator));
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
}
public Task HandleAsync(EventMessage message, CancellationToken cancellationToken)
{
ParentText = message.Text;
return Task.CompletedTask;
}
//public void NewWindow()
//{
//WindowManager wm = new WindowManager();
//SecondWindowViewModel swm = new SecondWindowViewModel(_eventAggregator);
//wm.ShowWindowAsync(swm);
//}
}
}
Now here instead of inheriting from Screen and IScreen, I inherit from Conductor because I want to have that ChildView on my form. NewWindow is how it worked before but now that button no longer works as I don't need to launch a new window anymore, that's why it is commented out.
As seen, on the contructor I subscribe _eventAggregator, and then HandleAsync does the job of receiving the message and assign it to a variable. Now on the Caliburn Documentatin the method to use is public void Handle() but that no longer works, that's the only way I managed to make it work.
Now when I run this the app does load and seems to work just fine, but as soon as the SendBack() method gets called (in SecondWindowViewModel) the line _eventAggregator.PublishOnUIThreadAsync(ToSend); launches an exception System.NullReferenceException: 'Object reference not set to an instance of an object.'
From my understanding EventAggregator should not care if I'm sending the message to a Window or a user panel or anything. Only thnig I changed is commenting out NewWindow and deleting the old SecondWindowWiew WPF Window and replacing it with a new SecondWindowView WPF User Control with the exact same XAML, then in ShellWiev added bellow a <ContentControl x:Name="ActiveItem"/>.
I'm a bit of a loss here, I've been trying everything, coping the code from the documentation, looking for tutorials online, other StackOverflow questions, and while I could make the UI load and make HandleAsync work, for 3 days straight I could not make it work with ChildViews. That code I wrote does work with a new window. I even run into problems of ShellView straight up not loading unless putting a new constructor that takes no parameters and empty inside, but that's for another day.
Sorry for the extra long post, but I think it's important to put all the information out there. Thank you for your time and again, sorry if this is somewhat of a dumb question, but we all have to start somewhere no?

Register views with regions depending on the user rights in Silverlight

We have a module class which implements the IModule interface and it has only one method Initialize() in which we register our views.
Is it possible to register these views after successful login?
I want to prohibit the registration of several views depending on the current user. But the user logs on after the module initialization.
Is there a way to provide a callback where Prism can evaluate if the registration is active? Or do I have the chance to disable registrations of the Region Manager? Any other ideas?
Thanks
The easiest way to communicate among multiple modules (or even within) in Prism is using the EventAggregator. Here is what I would do:
Create a CompositePresentationEvent for UserLoginEvent that takes relevant parameter regarding the user
Publish the event using IEventAggregator when the user successfully logs in
On your module initialize, subscribe to the UserLoginEvent, in the handler register the appropriate views
Repeat the above, but opposite, for UserLogout if desired.
In your infrastructure lib:
public class UserLoginEvent : CompositePresentationEvent<User> { }
Then in your module:
public class YourModule : IModule
{
private readonly IUnityContainer container;
private readonly IRegionManager regionManager;
private readonly IEventAggregator events;
public YourModule(IUnityContainer container, IRegionManager manager, IEventAggregator events)
{
this.container = container;
this.regionManager = manager;
this.events = events;
}
public void Initialize()
{
....
events.GetEvent<UserLoginEvent>().Subscribe(RegisterUserViews);
}
private void RegisterUserViews(User u)
{
// check user permissions
// register and create views using container, regionManager
}
}
In whatever module / code your user logs in... I assume you can get the IEventAggregator (similar to above) and then do something like:
OnUserLogin(User u)
{
eventAggregator.GetEvent<UserLoginEvent>().Publish(u);
}
Hope this helps! if a user logs out, then you may want to store references to your views in your module and remove them on a UserLogoutEvent which would work like the above code but just doing the opposite.

Mvvm Light Messenger message never recieved

I have a strange problem.
I'm using the MVVM Light framework from GalaSoft and everything so far works fine.
I'm using the messenger system to send messages between ViewModels just fine, until I tried to do the following:
I have a singleton class, GateKeeper, that sends messages.
This class is NOT a ViewModel and thus does not inherit from ViewModelBase.
If it sends a message it is not received anywhere.
I have tried the following:
Letting GateKeeper inherit from ViewModeBase -> No success.
Registering GateKeeper to receive messages, thus seeing if it would catch/receive the message that was actually sent from itself -> No Success.
Changing GateKeeper from Singleton to normal instantiation -> no Success
Creating a MVVM ViewModel that is not connected to a view, and letting it send messages, just like the GateKeeper -> no Success
All my viewModels that are connected to a view can send messages and they will be received.
It almost seems like a viewmodel must be "linked" to a view before the messenger works,but imo. that is a major limitation.
Below is the current, very simplified setup.
Calling ApplicationInitialize on GateKeeper does not trigger a message received on the mainviewmodel nor the GateKeeper class itselves.
I hope that someone has suggestions to this problem.
Thanks..
Example setup:
MainViewModel constructor:
public MainViewModel()
{
Messenger.Default.Register<LoadViewMessage>(this, (message) =>
{
if (message.Sender is GateKeeper) CurrentView = message.View;
else if (message.Sender is LoginViewModel) CurrentView = message.View;
else if (message.Sender is MenuItemBarViewModel) CurrentView = message.View;
});
GateKeeper:
public class GateKeeper : IGateKeeper
{
private readonly IEmployeeService _employeeService;
#region Implementation of IGateKeeper
public void ApplicationInitialize()
{
Messenger.Default.Send<LoadViewMessage>(new LoadViewMessage(ObjectLocator.MainMapView), this);
}
public void LoginSucceeded(Employee employee)
{
//This is where we retrieve the available services for the current employee
//TODO: add methods for retrieving service info from backend
//Send a message that should make the mainview load the map into its currentview property
Messenger.Default.Send(new LoadViewMessage(ObjectLocator.MainMapView), this);
}
#endregion
public GateKeeper(IEmployeeService employeeService)
{
_employeeService = employeeService;
//Test.. Is not triggered
//Just used for debugging, thus nothing happens inhere.
Messenger.Default.Register<LoadViewMessage>(this, (message) =>
{
if (message.Sender is GateKeeper) ;
else if (message.Sender is LoginViewModel) ;
else if (message.Sender is MenuItemBarViewModel);
});
}
Message class: LoadViewMessage
public class LoadViewMessage : MessageBase
{
public UserControl View { get; protected set; }
public LoadViewMessage(UserControl view, object sender): base(sender)
{
View = view;
}
public LoadViewMessage(UserControl view):this(view, null){}
}
PS: ObjectLocator is a NinJect class that handles all instantiation of objects and their lifecycle
#UPDATE
LBugnion (Creator of MVVM Light) pointed out that the problem lied in the send method, where i was actually using a overload of Send that takes a token.
#This will not work in my situation
Messenger.Default.Send(new LoadViewMessage(ObjectLocator.MainMapView), this);
#This WILL work
Messenger.Default.Send(new LoadViewMessage(ObjectLocator.MainMapView, this));
this was supposed to be passed to the loadViewMessage and NOT the Send method as a token
Your problem is on the Send method. You are using the overload of the method that takes a token as second parameter. You are passing "this" as token. It means that you are (probably by mistake) using the sender of the message as token.
If you are sending with a token, you also need to register the receiver with the same token (in that case, the exact same instance than the one used in the Send method). Since you didn't register with a token, the message is never sent by the Messenger, which is an optimization mechanism.
My guess is that you misunderstood the usage of the token in the Send method. Tokens are only here as a way to build a "private messenging network" if you want, where two objects can use the same token to register/send and establish a private communication.
In your case, if you want to send the Sender together with the message, you'll need to save the Sender in the message itself, which is what MessageBase and the derived classes do.
Hope this helps
Laurent

how to cleanup view model properly?

I have a view model that is used as the data source for my custom control. In the view model's constructor I set up a WMI ManagementEventWatcher and start it. My view model implements IDisposable, so I stop the watcher in the Dispose method.
When I embed the custom control into a window, and then close the window to exit the application it throws an InvalidComObjectException saying "COM object that has been separated from its underlying RCW cannot be used". This happens because of my watcher, and if I do not create it, there is no exception. there is no additional information about the exception such as stack trace, etc.
My guess is that something keeps the view model until the thread that the watcher uses terminates but before the watcher is stopped, and I do not know how to handle this.
Any advice?
Thanks
Konstantin
public abstract class ViewModelBase : IDisposable, ...
{
...
protected virtual void OnDispose() { }
void IDisposable.Dispose()
{
this.OnDispose();
}
}
public class DirectorySelector : ViewModelBase
{
private ManagementEventWatcher watcher;
private void OnWMIEvent(object sender, EventArrivedEventArgs e)
{
...
}
protected override void OnDispose()
{
if (this.watcher != null)
{
this.watcher.Stop();
this.watcher = null;
}
base.OnDispose();
}
public DirectorySelector()
{
try
{
this.watcher = new ManagementEventWatcher(new WqlEventQuery(...));
this.watcher.EventArrived += new EventArrivedEventHandler(this.OnWMIEvent);
this.watcher.Start();
}
catch (ManagementException)
{
this.watcher = null;
}
}
}
this article has the solution: Disposing WPF User Controls
basically, WPF dos not seem to use IDisposable anywhere, so the app needs to cleanup itself explicitly. so in my case, i subscribe to the Dispatcher.ShutdownStarted event from my control that uses the view model that needs to be disposed, and dispose the control's DataContext from the event handler.

Resources