mvvmcross iOS Messenger - - c#

Sorry in advance because I'm a newbie to this. I'm trying to trigger a method with a string in another viewmodel, and I've read that using MvxMessenger is basically my best option to do this.
What I don't understand because I can barely find any documentation/sample code to get me on my way is how to accomplish this
Whenever I click on a button in FilterViewModel, I also want it to trigger a method in the SearchHistoryViewModel with a string from the FilterViewModel.
Basically, if the SearchHistoryViewModel code is even correct, how do I send/Publish this message correctly?
FilterViewModel
public class SearchHistoryFilterViewModel : MvxViewModel
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value;
RaisePropertyChanged(() => Name);
}
}
public SearchHistoryFilterViewModel(IMvxMessenger messenger)
{
//_token = messenger.Subscribe
//_messenger = messenger;
}
public IMvxCommand FilterCommand
{
get
{
return new MvxCommand(FilterByName);
}
}
public void FilterByName()
{
//Whenever this method is triggered, send a message with the Name in it
SearchFilterMessage message = new SearchFilterMessage(this, Name);
//Send message
... ? /////////////////////
}`
SearchHistoryViewModel
public class SearchHistoryViewModel : MvxViewModel
{
//properties
...
...
private readonly MvxSubscriptionToken _token;
//ctor
public SearchHistoryViewModel(ISearchHistoryService searchHistoryService, IMvxNavigationService navigationService, IMvxMessenger messenger)
{
_searchHistoryService = searchHistoryService;
_navigationService = navigationService;
/*Subscribe - Whenever a SearchFilterMessage is received, trigger the
OnFilterMessage method */
_token = messenger.Subscribe<SearchFilterMessage>((message => {
OnFilterMessage(message.FilterName); })
);
}
//methods
....
....
....
/* Do this Whenever the SearchFilterMessage is received*/
public async void OnFilterMessage(string name)
{
HistoryItems = await _searchHistoryService.GetHistoryByName(name);
}

Alright, so I was confused on how to declare the messenger itself, and use it without the tokens, not in the constructor. Solution was predictably easy :(
Basically declare another ImvxMessenger and via injection set it, then call on that one to Publish it in another method
private IMvxMessenger _messenger;
public SearchHistoryFilterViewModel(IMvxMessenger messenger)
{
//_token = messenger.Subscribe...
//messenger.Publish<SearchFilterMessage>(FilterByName());
_messenger = messenger;
}
public void FilterByName()
{
Debug.WriteLine(Name);
SearchFilterMessage message = new SearchFilterMessage(this, Name);
//Send message
_messenger.Publish<SearchFilterMessage>(message);
}

Related

Changing TabbedPage binding property from a (different) class - Xamarin.Forms

My TabbedPage uses a Binding Property, which is defined in the tabbed page's ViewModel, for showing a Badge text.
I am setting the badge property when initializing the view (actually when it (re)appears). However, sometimes the badge text is changing from outside of my ViewModel(s), this is because I have a SignalR method which is called when a new message is being added by another application.
Though, when this happens the OnAppearing method of my tabbed viewmodel is obviously not called. So the question is, how can I 'notify' the tabbedpage viewmodel that the badge text should be changed.
I think the (best) way to do this is using somekind of Event. Since all of my ViewModels inherit from a 'ViewModelBase' I could implement the event notification / change in the ViewModelBase and override the property in my TabbedPage ViewModel.
Though, sadly my knowledge about using Events / EventArgs is limited and the stuff I found about it is not working.
Is using EventArgs the best way to solve this problem? And if so, could anyone give any pointers how to implement it properly.
*On a side-note, I am also using Prism
My TabbedPage ViewModel:
public class RootTabbedViewModel : ViewModelBase, IPageLifecycleAware
{
private readonly INavigationService _navigationService;
private int _messageCount;
public RootTabbedViewModel(INavigationService navigationService)
: base(navigationService)
{
_navigationService = navigationService;
}
public int MessageCount
{
get { return _messageCount; }
set { SetProperty(ref _messageCount, value); }
}
public void OnDisappearing()
{
}
void IPageLifecycleAware.OnAppearing()
{
// (omitted) Logic for setting the MessageCount property
}
}
ViewModelVase:
public class ViewModelBase : BindableBase, IInitialize, IInitializeAsync, INavigationAware, IDestructible, IActiveAware
{
public event EventHandler MessageAddedEventArgs; // this should be used to trigger the MessageCount change..
protected INavigationService NavigationService { get; private set; }
public ViewModelBase(INavigationService navigationService)
{
NavigationService = navigationService;
Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged;
IsNotConnected = Connectivity.NetworkAccess != NetworkAccess.Internet;
}
private bool _isNotConnected;
public bool IsNotConnected
{
get { return _isNotConnected; }
set { SetProperty(ref _isNotConnected, value); }
}
~ViewModelBase()
{
Connectivity.ConnectivityChanged -= Connectivity_ConnectivityChanged;
}
async void Connectivity_ConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
{
IsNotConnected = e.NetworkAccess != NetworkAccess.Internet;
if (IsNotConnected == false)
{
await DataHubService.Connect();
}
}
public virtual void Initialize(INavigationParameters parameters)
{
}
public virtual void OnNavigatedFrom(INavigationParameters parameters)
{
}
public virtual void OnNavigatedTo(INavigationParameters parameters)
{
}
public virtual void Destroy()
{
}
public virtual Task InitializeAsync(INavigationParameters parameters)
{
return Task.CompletedTask;
}
}
SignalR Datahub which should trigger the event:
public static class DataHubService2
{
// .. omitted some other SignalR specific code
public static async Task Connect()
{
try
{
GetInstanse();
hubConnection.On<Messages>("ReceiveMessage", async (message) =>
{
if(message != null)
{
// event that message count has changed should be triggered here..
}
});
}
catch (Exception ex)
{
// ...
}
}
}
As pointed out by #Jason, this specific problem is a good use case for using the MessagingCenter.
In the end the implementation looks as following:
public static class DataHubService2
{
// .. omitted some other SignalR specific code
public static async Task Connect()
{
try
{
GetInstanse();
hubConnection.On<Messages>("ReceiveMessage", async (message) =>
{
if(message != null)
{
MessagingCenter.Send("UpdateMessageCount", "Update");
}
});
}
catch (Exception ex)
{
// ...
}
}
}
public class RootTabbedViewModel : ViewModelBase, IPageLifecycleAware
{
private readonly INavigationService _navigationService;
private int _messageCount;
public RootTabbedViewModel(INavigationService navigationService)
: base(navigationService)
{
_navigationService = navigationService;
MessagingCenter.Subscribe<string>("UpdateMessageCount", "Update", async (a) =>
{
await UpdateMessageCount();
});
}
public int MessageCount
{
get { return _messageCount; }
set { SetProperty(ref _messageCount, value); }
}
public void OnDisappearing()
{
}
void IPageLifecycleAware.OnAppearing()
{
UpdateMessageCount();
}
async Task UpdateMessageCount()
{
int messageCount = await App.Database.GetNewMessageCountAsync();
MessageCount = messageCount.ToString();
}
}

How does a child ViewModel prompt the parent ViewModel to navigate away in Caliburn.Micro?

In Caliburn.Micro I have a Shell ViewModel that has 3 IShell properties corresponding to 3 content controls in the associated View. They are 'Full', 'List' and 'Detail'. 'Full' sits above the other two and is as wide as the host Form. 'List' is on the left hand 1 row down and 'Detail' is in the same row as 'List' 1 column to the right.
When the app starts, a Login ViewModel is bound to 'Full' and nothing is bound to the other two. The screen shows only the Login screen. The user should login, and when complete the 'Full' content control should switch from displaying the Login ViewModel, to an AccountViewModel.
For that to work I need the LoginViewModel to tell the ShellViewModel (its parent) to navigate to AccountViewModel.
How do I do that?
public class ShellViewModel : Screen
{
#region Fields
private string _title = "License Manager";
private Conductor<IScreen> _fullFrameConductor;
private Conductor<IScreen> _listFrameConductor;
private Conductor<IScreen> _detailFrameConductor;
#endregion
public ShellViewModel()
{
_fullFrameConductor = new Conductor<IScreen>();
_listFrameConductor = new Conductor<IScreen>();
_detailFrameConductor = new Conductor<IScreen>();
FullFrame = Framework.GetContainer().Resolve<LoginViewModel>();
}
#region Properties
public string Title { get => _title; set => _title = value; }
public IScreen FullFrame
{
get { return _fullFrameConductor.ActiveItem; }
set {
_fullFrameConductor.ActivateItem(value);
NotifyOfPropertyChange(nameof(FullFrame));
}
}
public IScreen ListFrame
{
get { return _listFrameConductor.ActiveItem; }
set {
_listFrameConductor.ActivateItem(value);
NotifyOfPropertyChange(nameof(ListFrame));
}
}
public IScreen DetailFrame
{
get { return _detailFrameConductor.ActiveItem; }
set {
_detailFrameConductor.ActivateItem(value);
NotifyOfPropertyChange(nameof(DetailFrame));
}
}
#endregion
#region Commands
public void ShowProducts()
{
ListFrame = Framework.GetContainer().Resolve<ProductListViewModel>();
DetailFrame = Framework.GetContainer().Resolve<ProductViewModel>();
}
public void ShowLicenses()
{
ListFrame = Framework.GetContainer().Resolve<LicenseListViewModel>();
DetailFrame = Framework.GetContainer().Resolve<LicenseViewModel>();
}
#endregion
}
public class LicenseViewModel : Screen
{
public void Login()
{
// This should process the login and then tell the Shell it is done
// then the shell should navigate to the Account ViewModel sharing
// the user info with the AccountViewModel via a memory cache
// How do I alert the screen ViewModel causing it to close this viewmodel
// without causing a threading problem?
}
}
You can make use of Event Aggregator to communicate between LoginViewModel and ShellViewModel. You can read more on Event Aggregator here.
First, you need to create a Message Class
public class AuthenticationSuccessMessage
{
public bool IsValidLogin{get;set;}
}
Then next step is to use EventAggregator to notify the ShellViewModel from the LicenseViewModel .
private IEventAggregator _eventAggregator;
public LicenseViewModel (IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}
public void Login()
{
_eventAggregator.PublishOnUIThread(new AuthenticationSuccessMessage{IsValidLogin=true});
}
The final step is to subscribe to the Events in ShellViewModel.
public class ShellViewModel:Screen, IHandle<AuthenticationSuccessMessage>
{
private readonly IEventAggregator _eventAggregator;
public ShellViewModel:Screen(IEventAggregator eventAggregator) {
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
}
void Handle<AuthenticationSuccessMessage>(AuthenticationSuccessMessage message)
{
if(message.IsValidLogin)
{
// Do Task
}
}
}
You can read more on Event Aggregators here.
Update : Do not forget to subscribe to Event Aggregator in ShellViewModel.

Conditional settings in WPF application

I'm making user changeable settings for my media player and I'm struggling to find an elegant solution to the problem.
One of my settings for example - pauses the video at it's last frame, if not checked it will either continue through the playlist or if it's only 1 file, reset it and pause it at the start.
This is how I've implemented it:
private void OnMediaEndedCommand()
{
if (GeneralSettings.PauseOnLastFrame)
{
MediaPlayer.SetMediaState(MediaPlayerStates.Pause);
return;
}
if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
{
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
return;
}
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
}
This is contained inside the ViewModel of the main window, where the media element is and GeneralSettings.PauseOnLastFrame is a boolean property.
This command is binded as follows:
<MediaElement ....>
<ia:Interaction.Triggers>
<ia:EventTrigger EventName="MediaEnded">
<ia:InvokeCommandAction Command="{Binding MediaEndedCommand}"/>
</ia:EventTrigger>
</ia:Interaction.Triggers>
</MediaElement>
It works but it's awful, how should I go about implementing such setting system in an elegant way? Some settings might not be boolean, they might have multiple options, some might be applied only on startup and others, as the one illustrated above, event based.
Based on the information and sample code you provided, I would suggest
Approach - 1
A tightly couple ViewModel with System.Configuration.ApplicationSettingsBase and you can mention all you properties in ViewModel and map single of them with a separate application setting property. You can use your settings directly in biding afterwards e.g. : {x:Static Settings.Default.Whatevs}. Othe "Save" button click event or main window close event, you can save all you settings e.g. : Settings.Default.Save();
Approach - 2
A better approach, I would suggest / prefer (if I am developing this app) is to develop a wrapper class (e.g.: SettingProvider) that implement an inheritance (e.g: ISettingProvider) which uncovers all you settings as separate properties and also have a save method which saves all setting values. You can use this wrapper class into your ViewModel to handle all the commands and setting values in better way.
The benefit of this approach is the if you decide to change you setting to database , you need not to make change to you ViewModel as all job is done in SettingProvider class.
I am not sure but based on viewing your code, I assume that you used Approach-1. Please put you comments and any feedback to this answer. I would like to know what you think and may be you have got more simple and interesting way of achieving this.
UPDATE-1
Example
Enum for showing you demo
public enum MediaStatus
{
Playing = 0,
Stopped = 1,
Paused = 2
}
Interface
public interface ISettingProvider
{
double Volumne { get; set; }
string LastMediaUrl { get; set; }
MediaStatus PlayingMediaStatus;
void SaveSettings();
}
Wrapper Class
public class SettingProvider : ISettingProvider
{
private double volumne;
public double Volumne // read-write instance property
{
get
{
return volumne;
}
set
{
volumne = value;
Settings.Default.Volumne = volumne;
}
}
private string lastMediaUrl;
public string LastMediaUrl // read-write instance property
{
get
{
return lastMediaUrl;
}
set
{
lastMediaUrl = value;
Settings.Default.LastMediaUrl = lastMediaUrl;
}
}
private MediaStatus playingMediaStatus;
public MediaStatus PlayingMediaStatus // read-write instance property
{
get
{
return playingMediaStatus;
}
set
{
playingMediaStatus = value;
Settings.Default.PlayingMediaStatus = (int)playingMediaStatus;
}
}
public void SaveSettings()
{
Settings.Default.Save();
}
//Constructor
public SettingProvider()
{
this.Volumne = Settings.Default.Volumne;
this.LastMediaUrl = Settings.Default.LastMediaUrl;
this.PlayingMediaStatus = (MediaStatus)Settings.Default.PlayingMediaStatus;
}
}
ViewModelBase Class
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
CommandHandler Class
public class CommandHandler : ICommand
{
public event EventHandler CanExecuteChanged { add { } remove { } }
private Action<object> action;
private bool canExecute;
public CommandHandler(Action<object> action, bool canExecute)
{
this.action = action;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute;
}
public void Execute(object parameter)
{
action(parameter);
}
}
ViewModel
public class SettingsViewModel : ViewModelBase
{
SettingProvider objSettingProvider = new SettingProvider();
public double Volumne
{
get
{
return objSettingProvider.Volumne;
}
set
{
objSettingProvider.Volumne = value;
OnPropertyChanged("Volumne");
}
}
// Implementaion of other properties of SettingProvider with your ViewModel properties;
private ICommand saveSettingButtonCommand;
public ICommand SaveSettingButtonCommand
{
get
{
return saveSettingButtonCommand ?? (saveSettingButtonCommand = new CommandHandler(param => saveSettings(param), true));
}
}
private void saveSettings()
{
objSettingProvider.SaveSettings();
}
}
UPDATE-2
public interface ISettingProvider
{
bool PauseOnLastFrame;
bool IsAutoPlay;
MediaStatus PlayingMediaStatus;
void SaveSettings();
}
public class SettingProvider : ISettingProvider
{
private bool pauseOnLastFrame;
public bool PauseOnLastFrame // read-write instance property
{
get
{
return pauseOnLastFrame;
}
set
{
pauseOnLastFrame = value;
Settings.Default.PauseOnLastFrame = volumne;
}
}
private bool isAutoPlay;
public bool IsAutoPlay // read-write instance property
{
get
{
return isAutoPlay;
}
set
{
isAutoPlay = value;
Settings.Default.IsAutoPlay = volumne;
}
}
}
public class SettingsViewModel : ViewModelBase
{
SettingProvider objSettingProvider = new SettingProvider();
MediaStatus PlayingMediaStatus
{
get
{
return objSettingProvider.PlayingMediaStatus;
}
set
{
if(value == MediaStatus.Paused)
MediaPlayer.Pause();
if(value == MediaStatus.Playing)
MediaPlayer.Play();
if(value == MediaStatus.Stopped)
MediaPlayer.Stop();
objSettingProvider.PlayingMediaStatus = (int)value;
OnPropertyChanged("PlayingMediaStatus");
}
}
private string currentMediaFile;
public string CurrentMediaFile
{
get
{
return currentMediaFile;
}
set
{
currentMediaFile = value;
MediaPlayer.Stop();
MediaPlayer.Current = currentMediaFile;
if(objSettingProvider.IsAutoPlay)
MediaPlayer.Play();
OnPropertyChanged("CurrentMediaFile");
}
}
// Implementaion of other properties of SettingProvider with your ViewModel properties;
private ICommand onMediaEndedCommand;
public ICommand OnMediaEndedCommand
{
get
{
return onMediaEndedCommand ?? (onMediaEndedCommand = new CommandHandler(param => onMediaEnded(param), true));
}
}
private void onMediaEnded()
{
if(objSettingProvider.PauseOnLastFrame)
{
PlayingMediaStatus = MediaStatus.Paused;
}
else if(PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
{
PlayingMediaStatus = MediaStatus.Stopped;
}
else
{
CurrentMediaFile = PlayListViewModel.FilesCollection.MoveNext();
}
}
}
NOTE: This is the detailed example I put here and also avoid some syntax error or naming error if I missed somewhere. Please correct it.
I am not aware which media player settings you are using. I took some sample properties. This is just an example of structure you can implement for you application. You may need to alter more code to implement this structure.
An elegant way to implement this IMHO would be to use a dependency injection container, this will provide great flexibility while allowing you to completely separate concerns (i.e. the settings implementation from your view models and custom controls).
There are many DI frameworks out there, for my example I will use simple injector because it is free (open source), simple and fast but you can apply the same principle to other frameworks (Unity, Ninject, etc..).
Start by creating an interface for your settings service, for example:
public interface ISettingsService
{
double Volumne { get; set; }
string LastMediaUrl { get; set; }
MediaStatus PlayingMediaStatus;
void SaveSettings();
}
Then add your implementation for the service, the beauty of using DI is that you can change the implementation at anytime or completely replace it and your application will continue to work as usual.
Let's say you want to use application settings, here is your service:
public class SettingsServiceFromApplication : ISettingsService
{
public double Volume
{
get
{
return Properties.Settings.Volume;
}
}
[...]
}
Or let's say you want to use a database to store your settings:
public class SettingsServiceFromDb : ISettingsService
{
public double Volume
{
get
{
return MyDb.Volumen;
}
}
[...]
}
Then you can use a DI container to specify which implementation to use:
Start by installing the library using NuGet:
Install-Package SimpleInjector -Version 4.0.12
You need a way to share your container throughout the application, I usually just go with a static class that I initialize when starting the app:
using Container = SimpleInjector.Container;
namespace YourNamespace
{
public class Bootstrapper
{
internal static Container Container;
public static void Setup()
{
//Create container and register services
Container = new Container();
//Let's specify that we want to use SettingsServiceFromApplication
Container.Register<ISettingsService, SettingsServiceFromApplication>();
//You can use your bootstrapper class to initialize other stuff
}
}
You need to call Setup when starting the App, the best place is in the App constructor:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Bootstrapper.Setup();
}
}
So now you have an app wide depedency injection container that you can use to request "services" (specific implementations of an interface).
To get the settings implementation in your view models you could simply call the container as follows:
// This will return an instance of SettingsServiceFromApplication
ISettingsService settingsService = Bootstrapper.Container.GetInstance<ISettingsService>();
double volumen = settingsService.Volume;
To make it easier to work with, I usually create a base view model that will allow to get services more easyly, for example:
public abstract BaseViewModel
{
private ISettingsService _settings;
protected ISettingsService GeneralSettings
{
get
{
if (_settings == null)
_settings = Bootstrapper.Container.GetInstance<ISettingsService>();
return _settings;
}
}
}
Every view model inheriting from this class will have access to the settings:
public class YourViewModel : BaseViewModel
{
private void OnMediaEndedCommand()
{
if (GeneralSettings.PauseOnLastFrame)
{
MediaPlayer.SetMediaState(MediaPlayerStates.Pause);
return;
}
if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
{
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
return;
}
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
}
}
As you can see the code is the same as your code! But now the settings are coming from your container. Where is the elegance? Well, let's say that one year from now someone decides that you will store your settings in a database, what do you need to change in your code?
Container.Register<ISettingsService, SettingsServiceFromDb>();
A single line. Everything else should work as usual.
As well as view models, you could use this mechanism in your own controls:
public class MyMediaElement : UserControl //Or MediaElement and instead of commands you can override real events in the control code behind, this does not break the MVVM pattern at all, just make sure you use depedency properties if you need to exchange data with your view models
{
private void OnMediaEndedCommand()
{
//Get your settings from your container, do whatever you want to do depending on the settings
[...]
}
}
Then just use your control in your Views / ViewModels:
<local:MyMediaElement />
Yep, that's all you need because you handle everything in your User / Custom control, your view models doesn't need to care about how you handle settings in the control.
There are many options you can use to register containers, I recommend you take a look at the docs:
https://simpleinjector.org/index.html
https://simpleinjector.readthedocs.io/en/latest/index.html
I think maybe you are looking for an interface approach?
public interface IMediaEndedHandler
{
bool AlternateHandling(MediaPlayer player);
}
public class NullMediaEndedHandler : IMediaEndedHandler
{
public bool AlternateHandling(MediaPlayer player)
{
return false;
}
}
public class PauseOnLastFrameHandler : IMediaEndedHandler
{
public bool AlternateHandling(MediaPlayer player)
{
player.SetMediaState(MediaPlayerStates.Pause);
return true;
}
}
public class GeneralSettings
{
private bool pauseOnLastFrame;
private bool PauseOnLastFrame
{
get
{
return pauseOnLastFrame;
}
set
{
pauseOnLastFrame = value;
MediaEndedHandler = value
? new PauseOnLastFrameHandler()
: new NullMediaEndedHandler();
}
}
public IMediaEndedHandler MediaEndedHandler = new NullMediaEndedHandler();
}
Then:
private void OnMediaEndedCommand()
{
if (GeneralSettings.MediaEndedHandler.AlternateHandling(MediaPlayer))
return;
if (PlayListViewModel.FilesCollection.Last().Equals(PlayListViewModel.FilesCollection.Current) && !Repeat)
{
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
MediaPlayer.SetMediaState(MediaPlayerStates.Stop);
return;
}
ChangeMediaPlayerSource(PlayListViewModel.ChangeCurrent(() => PlayListViewModel.FilesCollection.MoveNext()));
}
This way, if your setting is, for example. an enum instead of a bool, you can specify a different implementation of the interface for each possible value.

MvxAppCompatDialogFragment cancellation via Back button is not completely working

Closing a MvxAppCompatDialogFragment via the back button doesn't seem to work completely. The button I clicked to trigger the dialog remains disabled after the dialog is dismissed. It's almost like the Task is stuck. If I change to MvxDialogFragment then the back button will close the dialog as expected, and the button I clicked to trigger the dialog is enabled again after the dialog is dismissed. I'm trying to use MvxAppCompatDialogFragment because I'm using MvxAppCompatActivity. Am I doing something wrong or is this a bug in MvvmCross 5.2.1?
Here is the ViewModel:
public class ConfirmationViewModel : MvxViewModel<ConfirmationConfiguration, bool?>, IMvxLocalizedTextSourceOwner
{
private readonly IMvxNavigationService _mvxNavigationService;
public ConfirmationViewModel(IMvxNavigationService mvxNavigationService)
{
_mvxNavigationService = mvxNavigationService;
}
public override void Prepare([NotNull] ConfirmationConfiguration parameter)
{
if (parameter == null) throw new ArgumentNullException(nameof(parameter));
Title = parameter.Title;
Body = parameter.Body;
PositiveCommandText = !string.IsNullOrEmpty(parameter.YesCommandText)
? parameter.YesCommandText
: LocalizedTextSource.GetText("Yes");
NegativeCommandText = !string.IsNullOrEmpty(parameter.NoCommandText)
? parameter.NoCommandText
: LocalizedTextSource.GetText("No");
}
private bool? _confirmationResult;
public bool? ConfirmationResult
{
get => _confirmationResult;
private set => SetProperty(ref _confirmationResult, value);
}
private string _title;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private string _body;
public string Body
{
get => _body;
set => SetProperty(ref _body, value);
}
private string _positiveCommandText;
public string PositiveCommandText
{
get => _positiveCommandText;
set => SetProperty(ref _positiveCommandText, value);
}
private string _negativeCommandText;
public string NegativeCommandText
{
get => _negativeCommandText;
set => SetProperty(ref _negativeCommandText, value);
}
private IMvxAsyncCommand _yesCommand;
public IMvxAsyncCommand PositiveCommand => _yesCommand ?? (_yesCommand = new MvxAsyncCommand(OnPositiveCommandAsync));
private async Task OnPositiveCommandAsync()
{
ConfirmationResult = true;
await _mvxNavigationService.Close(this, ConfirmationResult);
}
private IMvxAsyncCommand _noCommand;
public IMvxAsyncCommand NegativeCommand => _noCommand ?? (_noCommand = new MvxAsyncCommand(OnNegativeCommandAsync));
private async Task OnNegativeCommandAsync()
{
ConfirmationResult = false;
await _mvxNavigationService.Close(this, ConfirmationResult);
}
public IMvxLanguageBinder LocalizedTextSource => new MvxLanguageBinder("", GetType().Name);
public IMvxLanguageBinder TextSource => LocalizedTextSource;
}
public class ConfirmationConfiguration
{
public string Title { get; set; }
public string Body { get; set; }
public string YesCommandText { get; set; }
public string NoCommandText { get; set; }
}
Here is the View:
[MvxDialogFragmentPresentation(Cancelable = true)]
[Register(nameof(ConfirmationFragment))]
public class ConfirmationFragment : MvxAppCompatDialogFragment<ConfirmationViewModel>
{
public ConfirmationFragment()
{
RetainInstance = true;
}
public ConfirmationFragment(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
RetainInstance = true;
}
public override Dialog OnCreateDialog(Bundle savedInstanceState)
{
var builder = new AlertDialog.Builder(Activity)
.SetTitle(ViewModel.Title)
.SetMessage(ViewModel.Body)
.SetPositiveButton(ViewModel.PositiveCommandText, OnPositiveButton)
.SetNegativeButton(ViewModel.NegativeCommandText, OnNegativeButton);
return builder.Create();
}
private async void OnNegativeButton(object sender, DialogClickEventArgs e)
{
if (ViewModel.NegativeCommand.CanExecute())
{
await ViewModel.NegativeCommand.ExecuteAsync();
}
}
private async void OnPositiveButton(object sender, DialogClickEventArgs e)
{
if (ViewModel.PositiveCommand.CanExecute())
{
await ViewModel.PositiveCommand.ExecuteAsync();
}
}
}
I'm navigating to the dialog like this:
var confirmation = await Mvx.Resolve<IMvxNavigationService>().Navigate<ConfirmationViewModel, ConfirmationConfiguration, bool?>(
new ConfirmationConfiguration()
{
Body = "Hello, World!",
Title = "Testing"
});
If I change the base class from MvxAppCompatDialogFragment to MvxDialogFragment then it all works as expected.
This was indeed an issue in MvvmCross v5.2.1 (thanks for reporting!). As a workaround for now you can add this code in your DialogFragment class:
public override void OnCancel(IDialogInterface dialog)
{
base.OnCancel(dialog);
ViewModel?.ViewDestroy();
}
public override void DismissAllowingStateLoss()
{
base.DismissAllowingStateLoss();
ViewModel?.ViewDestroy();
}
public override void Dismiss()
{
base.Dismiss();
ViewModel?.ViewDestroy();
}
I've looked into this. What happens is that when you press the back button it closes the View without calling the NavigationSerivce.Close(). This prevents the result Task to be called and either set or cancel the action. I'm not sure if this is a bug or just behavior. A workaround could be to call Close from ConfirmationViewModel ViewDissapearing, or cancel the Task yourself.

MvvmCross - handle button click in viewmodel

I'm new to xamarin and mvvmcross and I would like to wire up a simple button click from my ios project to my viewmodel.
using System;
using MvvmCross.Binding.BindingContext;
using MvvmCross.iOS.Views;
using Colingual.Core.ViewModels;
namespace Colingual.iOS
{
public partial class LoginView : MvxViewController
{
public LoginView() : base("LoginView", null)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Perform any additional setup after loading the view, typically from a nib.
var set = this.CreateBindingSet<LoginView, LoginViewModel>();
set.Bind(Username).To(vm => vm.Username);
set.Bind(Password).To(vm => vm.Password);
set.Bind(btnLogin).To(vm => vm.MyAwesomeCommand);
set.Apply();
}
public override void DidReceiveMemoryWarning()
{
base.DidReceiveMemoryWarning();
// Release any cached data, images, etc that aren't in use.
}
}
}
I would like to wire up btnlogin to myawesomecommand.
using MvvmCross.Core.ViewModels;
namespace Colingual.Core.ViewModels
{
public class LoginViewModel : MvxViewModel
{
readonly IAuthenticationService _authenticationService;
public LoginViewModel(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}
string _username = string.Empty;
public string Username
{
get { return _username; }
set { SetProperty(ref _username, value); }
}
string _password = string.Empty;
public string Password
{
get { return _password; }
set { SetProperty(ref _password, value); }
}
public bool AuthenticateUser() {
return true;
}
MvxCommand _myAwesomeCommand;
public IMvxCommand MyAwesomeCommand
{
get
{
DoStuff();
return _myAwesomeCommand;
}
}
void DoStuff()
{
string test = string.Empty;
}
}
}
As you can see I've got mvxCommand with the name of MyAwesomecommand but I want to handle some logic from a button click in my other project. Anyone know what I should do?
I've done more looking around and found an answer here.
MvxCommand _myAwesomeCommand;
public IMvxCommand MyAwesomeCommand
{
get { return new MvxCommand(DoStuff); }
}
void DoStuff()
{
string test = string.Empty;
}
The idea is to have your mvxcommand getter which returns a new command that takes the method as a parameter.
When clicking the button btnLogin, you can access void DoStuff in viewmodel.

Categories

Resources