Loosely Coupled Intra-View Model Notifications w/Veto Option - c#

In the application I'm building, the user may perform something in one view (backed by a view model) that should trigger an action in one or more other view models. Each of these other vms needs the ability to veto (a.k.a. cancel) the action performed in the first v/vm pair.
Example:
User clicks on an account in a DataGrid control displayed by the accounts list view. DataGrid event handler traps the click and tells vm. Vm notifies other vms of proposed change.
Since user has made unsaved edits to a record in another other view, other vm tells first vm that the proposed selected account change is rejected.
When accounts list vm receives this rejection, it tells DataGrid to keep the selected account set as it was. If no rejection had been received, accounts list vm would have allowed DataGrid selected item change to occur.
A similar scenario would be when the user initiates application shutdown. Interested vms need a way to know that shutdown is proposed and have the option to cancel shutdown.
The view models should be loosely coupled, so direct event subscriptions between them is undesirable.
How would you suggest implementing this intra-view model communication?
Would use an event aggregator to "broadcast" an account changing event be a wise approach? The event argument object would include a bool Canceled property. A subscribing vm that wants to cancel the change would set Canceled = true.
Thank you,
Ben

I think your last paragraph suggests a good solution.
Using the MVVM Light Toolkit, I would use messaging with a callback to send the message out and allow any number of subscribers to call back with a cancellation.
public class AccountSelectedMessage : NotificationMessageAction<bool>
{
public AccountSelectedMessage(Account selectedAccount, Action<bool> callback) : base("AccountSelectedWithCancelCallback", callback)
{
SelectedAccount = selectedAccount;
}
public AccountSelectedMessage(object sender, Account selectedAccount, Action<bool> callback) : base(sender, "AccountSelectedWithCancelCallback", callback)
{
SelectedAccount = selectedAccount;
}
public AccountSelectedMessage(object sender, object target, Account selectedAccount, Action<bool> callback) : base(sender, target, "AccountSelectedWithCancelCallback", callback)
{
SelectedAccount = selectedAccount;
}
public Account SelectedAccount { get; private set; }
}
public class AccountListViewModel : ViewModelBase
{
public RelayCommand<Account> AccountSelectedCommand = new RelayCommand<Account>(AccountSelectedCommandExecute);
private void AccountSelectedCommandExecute(Account selectedAccount)
{
MessengerInstance.Send(new AccountSelectedMessage(this, AccountSelectionCanceled));
}
private void AccountSelectionCanceled(bool canceled)
{
if (canceled)
{
// cancel logic here
}
}
}
public class SomeOtherViewModel : ViewModelBase
{
public SomeOtherViewModel()
{
MessengerInstance.Register<AccountSelectedMessage>(this, AccountSelectedMessageReceived);
}
private void AccountSelectedMessageReceived(AccountSelectedMessage msg)
{
bool someReasonToCancel = true;
msg.Execute(someReasonToCancel);
}
}
As you can see, this process would need to be asynchronous and take into account that you don't know how many recipients of the message could cancel, or how long they would take to respond.

An EventAggregator is the way to go. We used the one from Prism. But we didn't take all of prism. just the classes related to the EventAggregator. We did create a DomainEvent for service layers assemblies where the GUI references are not available.

Related

Blazor StateHasChanged() doesnt update global class values on page

So im trying to implement a multi-site server-side blazor application that has two services implemented as singletons like this:
services.AddSingleton<MQTTService>();
services.AddHostedService(sp => sp.GetRequiredService<MQTTService>());
services.AddSingleton<DataCollectorService>();
services.AddHostedService(sp => sp.GetRequiredService<DataCollectorService>());
The MQTT Service is connecting to the broker and managing the subscriptions and stuff, while the DataCollectorService subscribes to an event from the MQTT Service to be notified when a new message arrives. The business logic with the received data is then happening within the DataCollectorService, stuff like interpreting the topic and the payload of the mqtt message. If its valid, the DataCollectorService stores the Data in a (example) global static class:
if (mqtt.IsTopic(topic, MQTTService.TopicDesc.FirstTopic))
{
if(topic.Contains("Data1"))
{
if(topic.Contains("Temperature"))
{
DataCenter.Data1.Temperature= Encoding.UTF8.GetString(message, 0, message.Length);
}
}
}
The DataCenter is just a static class in the namespace:
public static class DataCenter
{
public static DataBlock Data1 = new DataBlock();
public static DataBlock Data2 = new DataBlock();
public static string SetMode;
public class DataBlock
{
public string Temperature { get; set; }
public string Name{ get; set; }
}
}
My Goal with this approach is that every different page in my project can just bind these global variables to show them.
The first problem that occurs then is that obviously the page is not aware of the change if the DataCollectorService updates a variable. Thats why i implemented a notifying event for the pages, which can then call StateHasChanged. So my examplePage "Monitor" wants to just show all these values and injects the DataCollectorService:
#page "/monitor"
#inject DataCollectorService dcs
<MudText>DataBlock Data1: #DataCenter.Data1.Temperature/ Data2: #DataCenter.Data2.Temperature</MudText>
#code
{
protected override void OnInitialized()
{
dcs.OnRefresh += OnRefresh;
}
void OnRefresh()
{
InvokeAsync(() =>
{
Console.WriteLine("OnRefresh CALLED");
StateHasChanged();
});
}
}
This actually works, but adds a new problem to the table, everytime i switch to my monitor site again a NEW OnRefresh Method gets hooked to the Action and that results in multiple calls of "OnRefresh". I find this behaviour rather logical, cuz i never delete an "old" OnRefresh Method from the Action when I'm leaving the site, cuz i dont know WHEN i leave the site.
Thinking about this problem i came up with a solution:
if (!dcs.IsRegistered("monitor"))
{
dcs.OnRefresh += OnRefresh;
dcs.RegisterSubscription("monitor");
}
I wrapped the action subscription with a system that registers token whenever the handler is already correctly assigned. the problem now: the variables on the site dont refresh anymore!
And thats where i'm not sure how to understand whats going on anymore. If i keep it like in the first example, so just adding dcs.OnRefresh += OnRefresh; and letting it "stack up", it actually works - because there is always a "new" and "correctly" bound method which, in my limited understanding, has the correct context.
if i forbid this behaviour i only have an somehow "old" method connected which somehow cant execute the StateHasChanged correctly. But i dont know why.
I'm not sure if i could:
"Change" the context of the Invoke Call so that StateHasChanged works again?
Change the way I register the Action Handling method
I'm additionally confused as to why the first way seems to call the method multiple times. Because if its not able to correctly call StateHasChanged() in the old method, why can it be called in the first place?
I would very much appreciate some input here, googling this kind of stuff was rather difficult because i dont know the exact root of the problem.
Not only do you have multiple calls, you also have a memory leak. The event subscription will prevent the Monitor object to be collected.
Make the page IDisposable:
#page "/monitor"
#inject DataCollectorService dcs
#implements IDisposable
...
#code
{
protected override void OnInitialized()
{
dcs.OnRefresh += OnRefresh;
}
...
public void Dispose()
{
dcs.OnRefresh -= OnRefresh;
}
}

How to correctly send event-messages between modules in Prism framework?

I am trying to build a WPF Prism bases app using MVVM design pattern.
When my app first starts, I want to require the user to login. Once logged in, I want to show the default landing page with the user name and buttons.
My thought, when a user login, I would publish an event called UserLoggedIn then on the home view-model, I would listen for that event. When the event is triggered, I would show the landing/home view.
So I created the event like so
public class UserLoggedIn : PubSubEvent<User>
{
}
Then in the LoginViewModel I handle the login and publish the event like so
private void HandleLogin(LoginView loginView)
{
try
{
User user = AuthenticationService.Authenticate(Username, loginView.GetPassport());
IUserPassport passport = PassportManager.Get(user.Username);
if (passport == null)
{
// Create a new session
passport = new UserPassport(new CustomIdentity(user), RegionManager);
}
// Assign the current session
PassportManager.SetCurrent(passport);
// Trigger the event
EventAggregator.GetEvent<UserLoggedIn>().Publish(passport);
// Deactivate the login view
RegionManager.GetMainRegion().Deactivate(loginView);
}
catch (Exception ex)
{
//Do something with the error
}
}
Finally in my HomeViewModel aka my landing view, I have the following code to listen for the UserLoggedIn event.
public class HomeViewModel : BindableBase
{
protected IUnityContainer Container { get; set; }
protected ICoreRegionManager RegionManager { get; set; }
private IEventAggregator EventAggregator { get; set; }
public HomeViewModel(IUnityContainer container, ICoreRegionManager regionManager, IEventAggregator eventAggregator)
{
Container = container;
RegionManager = regionManager;
EventAggregator = eventAggregator;
eventAggregator.GetEvent<UserLoggedIn>().Subscribe(ShowTheLangingPage);
}
private void ShowTheLangingPage(User user)
{
var homeView = Container.Resolve<HomeView>();
RegionManager.AddToMainRegion(homeView);
FullName = user.FirstName;
}
// I am using PropertyChange.Fody package, so this propery will automaticly raise the PropertyChange event.
public string FullName { get; set; }
}
Problem is the ShowTheLangingPage method never get triggered in my HomeViewModel as expected.
I made sure the the View HomeView and HomeViewModel are wired up correctly, by directly loading the HomeView on module initialization for testing.
Additionally, if add Container.Resolve<HomeView>(); just before I publish the event, the ShowTheLangingPage I called. Its like I have to resolve the HomeView manually for it to listen for the event.
How can I correctly listen for the UserLoggedIn event so i can show the corresponding view.
So I can learn the better/recommended way, is it better to show the landing view from the LoginViewModel instead of using event/listener.... and why? Also, if showing the landing view directly from the LoginViewModel then what is the recommended method to navigate; using Region.Add() method or RegionManager.RequestNavigate?
is it better to show the landing view from the LoginViewModel instead of using event/listener....
Yes.
and why?
Because that's what services (like the IRegionManager) are for, doing stuff for your view models and other services. Also, you have noticed, events can only be subscribed to by living objects.
Also, if showing the landing view directly from the LoginViewModel then what is the recommended method to navigate; using Region.Add() method or RegionManager.RequestNavigate?
If anything, a third class should listen for UserLoggedIn, but that's no gain over using the IRegionManager directly. In fact, it's even worse, because you have to artifically create this class. Side note: if you wait for the garbage collector after Container.Resolve<HomeView>(); and before logging in, you won't go to the landing page, because there's no subscriber (again).

How to clear data of ViewModel in MVVM Light xamrin?

I am working on Xamrin Form right now. I have problem with clear data of ViewModel.
When I logout and login with different user, it shows me data of previous user because the value of UserProfileViewModel doesn't get clear.
When user logout, I want to clear user data from UserProfileViewModel class file. Currently I do this manually when user click on logout. I want any default method like dispose to clear all class member.
I have tried to inherit IDisposable interface with this.Dispose(); but that also didn't work.
I have also tried with default constructor as following but it throws error of
`System.TypeInitializationException`
on this line in app.xaml.cs: public static ViewModelLocator Locator => _locator ?? (_locator = new ViewModelLocator());
public UserProfileViewModel()
{
//initialize all class member
}
In given code, you can see that on Logout call, I call method
`ClearProfileData` of `UserProfileViewModel`
which set default(clear)
data. It is manually. I want to clear data when user logout.
View Model Logout Page
[ImplementPropertyChanged]
public class LogoutViewModel : ViewModelBase
{
public LogoutViewModel(INavigationService nService, CurrentUserContext uContext, INotificationService inService)
{
//initialize all class member
private void Logout()
{
//call method of UserProfileViewModel
App.Locator.UserProfile.ClearProfileData();
//code for logout
}
}
}
User Profile View Model
[ImplementPropertyChanged]
public class UserProfileViewModel : ViewModelBase
{
public UserProfileViewModel(INavigationService nService, CurrentUserContext uContext, INotificationService inService)
{
//initialize all class member
}
//Is there any other way to clear the data rather manually?
public void ClearProfileData()
{
FirstName = LastName = UserName = string.Empty;
}
}
ViewModel Locator
public class ViewModelLocator
{
static ViewModelLocator()
{
MySol.Default.Register<UserProfileViewModel>();
}
public UserProfileViewModel UserProfile => ServiceLocator.Current.GetInstance<UserProfileViewModel>();
}
Firstly there is no need to cleanup these kinds of primitive data types, the gc will do that for you.
However if you use Messages or any other Strong Reference for that matter you WILL have to Unsubscribe from them otherwise your viewmodal will hang around in memory and will never go out of scope
The garbage collector cannot collect an object in use by an
application while the application's code can reach that object. The
application is said to have a strong reference to the object.
With Xamarin it really depends how you are coupling your View to Viewmodals to determine which approach you might take to cleanup your viewmodals.
As it turns out MVVM Light ViewModelBase implements an ICleanup interface which has an overridable Cleanup method for you.
ViewModelBase.Cleanup Method
To cleanup additional resources, override this method, clean up and
then call base.Cleanup().
public virtual void Cleanup()
{
// clean up your subs and stuff here
MessengerInstance.Unregister(this);
}
Now your just left with where to call ViewModelBase.Cleanup
You can just call it when your View Closes, if you get a reference to the DataContext (I.e ViewModalBase) on the DataContextChanged Event
Or you can wire up a BaseView that plumbs this for you, or you can implement your own NagigationService which calls Cleanup on Pop. It really does depend on who is creating your views and viewmodels and how you are coupling them

Do functions when closing and when changing ViewModel - C# using MVVM

I have some questions and I'm having some trouble finding answers so I decided to put them here.
Q1: I have to make to call a function each time the app closes, like: click exit button and then do something.
Q2: I have a menuitemcontrol in the shell viewmodel that controls the ViewModel but on creating them I do some webservices requests, but imagine I delete a friend it is requested that I update the request in the viewmodel, how can I do this calling from other viewmodels?
EDIT: Scenario - ShellViewModel that contains HomeViewModel and FriendsViewModel, I accepted a friend in the FriendsViewModel I want that when I click Home the function that fetch the info from the webservice to run again. (If I was doing in code-behind I would use Onclick[Home] > runlogin())
UpdateQ2:
public FriendsViewModel()
{
MessengerInstance.Register<NotificationMessage>(this, NotifyMe);
au = AuthSingleton.Instance.getAuthUser(); // Singleton that works like a session in the desktop App.
if (AuthSingleton.Instance.IsAuth == true)
loadFriends();
}
public void NotifyMe(NotificationMessage notificationMessage)
{
string notification = notificationMessage.Notification;
//do your work
loadFriends();
}
#endregion constructors
public async void loadFriends()
{
var response = await CommunicationWebServices.GetASM(au.idUser + "/friends", au.token);
var fh = JsonConvert.DeserializeObject<FriendsHandler>(response);
}
I've decided to use a suggestion from a commenter user to send a message from the second ViewModel to this one to order the update to run again (pretty cool and easy solution), but it doesn't work because somehow my singleton is deleted :O
Message sent: MessengerInstance.Send(new NotificationMessage("notification message"));
Best regards,
Q1 - What MVVM framework are you using? All MVVM frameworks I know implement custom commands (aka RelayCommands/DelegatingCommands), so you could attach them to Window events. Another solution would have in your ViewModel an implementation of ClosingRequest event. Something like this:
public class BaseViewModel
{
public event EventHandler ClosingRequest;
protected void OnClosingRequest()
{
if (this.ClosingRequest != null)
{
this.ClosingRequest(this, EventArgs.Empty);
}
}
}
So, in your View you would have:
public partial class MainWindow: Window
{
...
var vm = new BaseViewModel();
this.Datacontext = vm;
vm.ClosingRequest += (sender, e) => this.Close();
}
If you are using MVVM Light, you can do the following in your ViewModel:
public ICommand CmdWindowClosing
{
get
{
return new RelayCommand<CancelEventArgs>(
(args) =>{
});
}
}
And in your Window:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<command:EventToCommand Command="{Binding CmdWindowClosing}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
Q2 - Also, this is easy when using a MVVM framework. Most of them implement the Message Mediator pattern. What this mean for your. This mean you can dispatch a message warning "Request needs update" and a receptor binds to that message, implementing something when the message is received. Take a look on this demo from Microsoft

C# MVVM Handling and Passing UserID to different Models and ViewModels

My current implementation of passing UserID in my application is through the constructor.
i.e. SomeObject s = new SomeObject(userID)
Where in there is a code behind that does things based on the userID. The userID is further keep tracked by adding another property named "CurrentUser", however this seems to be a dirty solution as I have to implement it to all ViewModels and it seems to violate the "DRY" concept.
The second approach I have in mind is creating a public static variable on my MainWindowViewModel where all my other models can refer to it as MainWindowViewModel.CurrentUser.
Is one of the two approach the correct way to do this or is there a better approach that i don't know about?
You need to carefully analyze up front what you want to achieve with your application. Are you happy with there only ever being one selected client? Or will you need to have multiple clients being viewed or edited at a time (i.e. you have an MDI style app)?
Going with the single client approach is easy, you can implement the global property bag as already mentioned in other answers. But I will advise caution: if you build your app on the assumption there will only ever be one selected client it becomes a real PITA to try to refactor to make it multi-client capable. Using a centralized property bag or "session service" like this is indeed decoupling state from the VM, but the centralized service can still turn into a monstrosity over time and you build up too much dependence on it.
If you do want to go the multi-client route, then you are on the right track - but instead of passing a client identifier in on the constructor, pass (inject) the entire client data object. The chances are that you already have most of the client details available from the piece of UI that invokes the client oriented VM, so pass it in and save having to make another trip to your database to get the details.
Don't tie a current user to a ViewModel. I typically opt for a SessionService of some kind. If you're using Dependency Injection (DI), register a singleton of an ISessionService and concrete implementation. If your not using DI, then just have your app start create a singleton, like a SessionService.Current. Then you can put any items you need in here. Then each ViewModel can ask for the SessionService.Current.User and they have it. Your ViewModels shouldn't know about each other, but they can know about services. This keeps it DRY and loosely coupled, especially if you only access these session variables using the interface of an ISessionService and not the concrete implementation. This allows you to mock one up very easily without changing any ViewModel code.
What you have here is the problem of Communication between ViewModels. There are a number of solutions but my fave is the Mediator Pattern:
using System;
namespace UnitTestProject2
{
public class GetDataViewModel
{
IMediator mediator;
public GetDataViewModel(IMediator mediator)
{
this.mediator = mediator;
this.mediator.ListenFor("LoggedIn", LoggedIn);
}
protected string UserId;
protected void LoggedIn(Object sender, EventArgs e)
{
UserId = sender.ToString();
}
}
public class LoginViewModel
{
IMediator mediator;
public LoginViewModel(IMediator mediator)
{
this.mediator = mediator;
}
public string UserId { get; set; }
public void Login(string userid)
{
this.UserId = userid;
this.mediator.RaiseEvent("LoggedIn", this.UserId);
}
}
public interface IMediator
{
public void ListenFor(string eventName, EventHandler action );
public void RaiseEvent(string eventName, object data);
}
}
I Haven't implemented the Mediator here, because it can get quite involved and there are a number of packages available. but you can see the idea from my simple interface. Essentially the Mediator provides a Global list of EventHandlers which any Viewmodel can call or add to. You still have the problem of where to store the event names. Its nice to have these in enums, but that gives you a coupling problem. (a problem I usually ignore)
Alternatively you can have a Controller or (MasterViewModel if you love MVVM)
using System;
namespace UnitTestProject3
{
public class GetDataViewModel
{
protected string UserId;
public void LoggedIn(Object sender, EventArgs e)
{
UserId = sender.ToString();
}
}
public class LoginViewModel
{
public EventHandler OnLogin;
public string UserId { get; set; }
public void Login(string userid)
{
this.UserId = userid;
if (this.OnLogin != null)
{
this.OnLogin(this.UserId, null);
}
}
}
public class Controller // or MasterViewModel
{
public void SetUp()
{
GetDataViewModel vm1 = new GetDataViewModel();
LoginViewModel vm2 = new LoginViewModel();
vm2.OnLogin += vm1.LoggedIn;
//wire up to views and display
}
}
}

Categories