Is it okay to include methods in a view model or should they be placed in another part of the code? - xamarin

My Xamarin Forms ViewModel looks like this:
public class CFSPageViewModel : BaseViewModel
{
#region Constructor
public CFSPageViewModel()
{
PTBtnCmd = new Command<string>(PTBtn);
OnTappedCmd = new Command<string>(OnTapped);
}
#endregion
# region Commands
public ICommand PTBtnCmd { get; set; }
public ICommand OnTappedCmd { get; }
#endregion
#region Methods
private void OnTapped(string btnText)
{
Utils.SetState(btnText, CFS, SET.Cfs);
CFSMessage = Settings.cfs.TextLongDescription();
}
private void PTBtn(string btnText)
{
Utils.SetState(btnText, PT);
SetLangVisible(btnText);
SetLangSelected(btnText);
CFSMessage = Settings.cfs.TextLongDescription();
}
}
I was previously sending a message with MessageCenter to my C# back end code but now have removed MessageCenter so the methods are part of the ViewModel.
Is this a safe thing to do? I heard that MessageCenter messages passing around between ViewModels for everything was not the best of solutions.
Note that here is the way I had been doing it before:
MyPageViewModel.cs
PTBtnCmd = new Command<Templates.WideButton>((btn) =>
MessagingCenter.Send<CFSPageViewModel, Templates.WideButton>(
this, "PTBtn", btn));
MyPage.xaml.cs
MessagingCenter.Subscribe<CFSPageViewModel, Templates.WideButton>(
this, "PTBtn", (s, btn) =>
{
Utils.SetState(btn.Text, vm.PT);
SetLangVisible(btn.Text);
SetLangSelected(btn.Text);
vm.CFSMessage = Settings.cfs.TextLongDescription();
});
Note that methods such as SetLangVisible were also in MyPage.xaml.cs

To add an event handler to your Buttonsimply:
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyProject.Views.MyPage">
<ContentPage.Content>
<StackLayout>
<Button Text="PTBtn" Clicked="Handle_Clicked" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
In code behind:
namespace MyProject.Views
{
public partial class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
}
void Handle_Clicked(object sender, EventArgs eventArgs)
{
((Button)sender).BackgroundColor = Color.Blue; // sender is the control the event occured on
// Here call your methods depending on what they do/if they are view related
/*
Utils.SetState(btn.Text, vm.PT);
SetLangVisible(btn.Text);
SetLangSelected(btn.Text);
vm.CFSMessage = Settings.cfs.TextLongDescription();
*/
}
}
}
All the events that can have a event handler assigned to it is listed in yellow with E:
The Command fires first and you can add CanExecute as a second parameter in the constructor - which will also stop both the command and the event handler from being executed.
I would also rename the Command to something like SelectLanguageCommand - to distinguish it from a ui action. That way you can disconnect the button from the command and connect the command to other ui - if you decide you want to change the view in the future. It would also be easier to understand when unit testing.
Is this a safe thing to do? I heard that MessageCenter messages passing around between ViewModels for everything was not the best of solutions.
You could register all your view models with DependencyService
public App()
{
InitializeComponent();
DependencyService.Register<AboutViewModel>();
DependencyService.Register<CFSPageViewModel>();
DependencyService.Register<MyPageViewModel>();
MainPage = new AppShell();
}
Set BindingContext of the views to the instances registered:
public AboutPage()
{
InitializeComponent();
BindingContext = DependencyService.Get<AboutViewModel>();
}
And get the ViewModel instance anywhere you need it. That way you don't have to deal with subscriptions as you need to when using MessagingCenter.
Wether it is safe or not - I am not sure.

Related

What do I need to do in my view to make Prism.Uno work with x:Bind?

I come from a WPF / Prism background but I really like what X:Bind offers. How do I get x:Bind to work with my ViewModel when using Prism.Uno?
I have prism:ViewModelLocator.AutoWireViewModel="True" but I seem to be missing something in my understanding of how it works when designing.
Thanks
G
The use of x:Bind requires the binding path to be rooted in the View.
To use the DataContext, you'll need make it available typed through the view, like this:
public partial class MyControl : INotifyPropertyChanged
{
#if !HAS_UNO
// Uno already defines this event (it will be removed
// in the future to be aligned properly with WinUI)
public event PropertyChangedEventHandler PropertyChanged;
#endif
public MainPage()
{
this.InitializeComponent();
DataContextChanged +=
(s, e) => PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(nameof(ViewModel)));
}
public MyViewModel ViewModel => DataContext as MyViewModel;
}

MessagingCenter and NavigationService using reflection

I'm looking to improve my mobile application developed in Xamarin.Forms.
My functionality is as follows: onResume of the application I want to reload the page on which the user was.
Currently I use the MessagingCenter to operate with the code below.
Unfortunately my application is starting to have a lot of pages and it's not very readable anymore.
I am therefore looking to pass my type (viewModel) as a parameter of my navigation service - my research directs me towards the concept of reflection but I don't know if my problem is achievable.
// App.xaml.cs
protected override void OnResume()
{
// Handle when your app resumes
Page currPage = ((NavigationPage)((MasterDetailPage)Application.Current.MainPage).Detail).CurrentPage;
MessagingCenter.Send<App, Page>(this, "Hi", currPage);
}
Then in my BaseViewModel :
// BaseViewModel.cs
public ViewModelBase()
{
DialogService = ViewModelLocator.Instance.Resolve<IDialogService>();
NavigationService = ViewModelLocator.Instance.Resolve<INavigationService>();
AuthenticationService = ViewModelLocator.Instance.Resolve<IAuthenticationService>();
MessagingCenter.Subscribe<App, Page>(this, "Hi", async (sender, arg) =>
{
// Do something whenever the "Hi" message is received
Type viewModel = NavigationService.GetViewModelTypeForPage(arg.GetType());
if(viewModel == typeof(AboutViewModel))
{
Debug.WriteLine("AboutViewModel");
await NavigationService.NavigateToAsync<AboutViewModel>();
return;
}
if (viewModel == typeof(CardViewModel))
{
Debug.WriteLine("CardViewModel");
await NavigationService.NavigateToAsync<CardViewModel>();
return;
}
...
});
}
I would give you some ideas on how to make your code readable when using MessagingCenter.
First, you can have a BasePage which implemented the MessagingCenter.Subscribe and a method which called loadData:
public partial class BasePage : ContentPage
{
public BasePage()
{
MessagingCenter.Subscribe<App, string>(this, "Hi", (sender, arg) =>
{
// Do something whenever the "Hi" message is received
loadData();
});
}
public virtual void loadData()
{
}
}
Then, when you create a new page which need to refresh when the application is resumed, you can make the page inherits from the BasePage type:
public partial class MainPage : BasePage
{
public MainPage()
{
InitializeComponent();
loadData();
}
public override void loadData()
{
base.loadData();
Console.WriteLine("loadData");
}
}
And the xaml:
<?xml version="1.0" encoding="utf-8" ?>
<bases:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:bases="clr-namespace:App52"
mc:Ignorable="d"
x:Class="App52.MainPage">
</bases:BasePage>
So you don't have to implement MessagingCenter.Subscribe in each Page, those can be managed in BasePage.
I'm not familiar with reflection so maybe can't help you on achieving that by reflection. Hope this helps.

Create Behavior from Map Property in Control - Xamarin Forms MVVM Prism ArcGIS

I'm trying to show a callout when navigating from View A (form view) to View B (map view). I pass the record detail for the callout from View A to View B and it's bound to viewmodel of View B.
I would like to access the LoadStatusChanged event on the Map property of the Esri MapView control while adhering to MVVM architecture. Here is what my control looks like:
<esri:MapView x:Name="mapViewMain"
Map="{Binding MainMap}"
Grid.Row="0"
InteractionOptions="{Binding MapViewOptions}"
GraphicsOverlays="{Binding GraphicsOverlays}">
<esri:MapView.Behaviors>
<bh:ShowCalloutOnTapBehavior CalloutClickCommand="{Binding GoToDetailCommand}" />
<bh:ShowCalloutOnDataReceivedBehavior MeterMasterRequest="{Binding RequestParameters}" Map="{Binding MainMap}" />
</esri:MapView.Behaviors>
</esri:MapView>
I think I need to create a behavior that will take in the Map, wait for it to finish load, then show callout (results of MeterMasterRequest) on MapView (the ShowCallout method is on MapView control).
public class ShowCalloutOnDataReceivedBehavior : BehaviorBase<MapView>
{
public static readonly BindableProperty MeterMasterRequestProperty =
BindableProperty.Create(nameof(MeterMasterRequest), typeof(MeterMasterRequest), typeof(ShowCalloutOnDataReceivedBehavior));
public static readonly BindableProperty MapProperty =
BindableProperty.Create(nameof(Map), typeof(Map), typeof(ShowCalloutOnDataReceivedBehavior));
public MeterMasterRequest MeterMasterRequest
{
get { return (MeterMasterRequest)GetValue(MeterMasterRequestProperty); }
set { SetValue(MeterMasterRequestProperty, value); }
}
public Map Map
{
get { return (Map)GetValue(MapProperty); }
set { SetValue(MapProperty, value); }
}
How can I bind to the Map event from here? I don't know how I can get BehaviorBase to be of type Map. I seem to only be able to set behaviors at MapView level.
You can access the Map from the MapView.Map property and the MapView is passed in to the OnAttachedTo and OnDetachingFrom methods of the Behavior class. I see you have a BehaviorBase, which hopefully has the OnAttachedTo and OnDetachingFrom overrides still marked as protected virtual so you can override them in your ShowCalloutOnDataReceivedBehavior class
Override the OnAttachedTo method and then subscribe to the LoadStatusChanged event, override OnDetachingFrom so you can unsubscribe like so:
protected override void OnAttachedTo(MapView bindable)
{
base.OnAttachedTo(bindable);
bindable.Map.LoadStatusChanged += Map_LoadStatusChanged;
}
protected override void OnDetachingFrom(MapView bindable)
{
base.OnDetachingFrom(bindable);
bindable.Map.LoadStatusChanged -= Map_LoadStatusChanged;
}
private void Map_LoadStatusChanged(object sender, Esri.ArcGISRuntime.LoadStatusEventArgs e)
{
// Do stuff when LoadStatusChanged event is fired on the Map
}

How do I create an alert popup from my ViewModel in Xamarin?

I'm developing my Xamarin app with the MVVM pattern. I want to display an alert to the user when the user presses a button.
I declare my ViewModel with
class MainPageViewModel : BindableBase {
Unfortunately, I'm not able to access a Page object from within the ViewModel directly. How do I best go about displaying my alert?
Late to the party but as Nick Turner has mentioned in a number of comments, the solutions given so far require the view model to reference the view which is an antipattern/infringement of MVVM. Additionally, you'll get errors in your view model unit tests such as: You MUST call Xamarin.Forms.Init(); prior to using it.
Instead you can create an interface that contains the code for your alert boxes and then use in your view model as follows:
Interface:
public interface IDialogService
{
Task ShowAlertAsync(string message, string title, string buttonLabel);
}
Implementation (Using ACR.UserDialogs NuGet package):
public class DialogService : IDialogService
{
public async Task ShowAlertAsync(string message, string title, string buttonLabel)
{
if (App.IsActive)
await UserDialogs.Instance.AlertAsync(message, title, buttonLabel);
else
{
MessagingCenter.Instance.Subscribe<object>(this, MessageKeys.AppIsActive, async (obj) =>
{
await UserDialogs.Instance.AlertAsync(message, title, buttonLabel);
MessagingCenter.Instance.Unsubscribe<object>(this, MessageKeys.AppIsActive);
});
}
}
}
ViewModel:
public class TestViewModel
{
private readonly IDialogService _dialogService;
public TestViewModel(IDialogService dialogService)
{
//IoC handles _dialogService implementation
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
}
public ICommand TestCommand => new Command(async () => await TestAsync());
private async Task TestAsync()
{
await _dialogService.ShowAlertAsync("The message alert will show", "The title of the alert", "The label of the button");
}
}
TestCommand can then be bound to the button in the your xaml:
<Button x:Name="testButton" Command="{Binding TestCommand}">
To display Alert write below code in your ViewModel class
public class MainViewModel
{
public ICommand ShowAlertCommand { get; set; }
public MainViewModel()
{
ShowAlertCommand = new Command(get => MakeAlter());
}
void MakeAlter()
{
Application.Current.MainPage.DisplayAlert("Alert", "Hello", "Cancel", "ok");
}
}
Set your Command to Button in xaml
<StackLayout>
<Button Text="Click for alert" Command="{Binding ShowAlertCommand}"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
Set BindingContext in code behind of your xaml file. If you xaml file MainPage.xaml
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
You can call the below code within the view model, if you are using normal MVVM pattern.
App.current.MainPage.DisplayAlert("","","");
You can use Prism's PageDialogService which keeps your ViewModels very clean and testable.
For Shell application I was able to achieve like this
await Shell.Current.DisplayAlert("Title", "Message", "Cancel");
Would it be a good idea to create popups like this? (called from my ViewModel)
private void OpenPopUp()
{
Application.Current.MainPage.Navigation.ShowPopup(new CustomPopUp());
}
You can find a guide on how to create custom pupups here:
https://www.youtube.com/watch?v=DkQbTarAE18
Works pretty good for me, but I am very new to Xamarin.

How to implement a custom presenter in a Windows UWP (Xamarin, MvvmCross)

I have the following code in my Android app, it basically uses one page (using a NavigationDrawer) and swaps fragments in/out of the central view. This allows the navigation to occur on one page instead of many pages:
Setup.cs:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var customPresenter = new MvxFragmentsPresenter();
Mvx.RegisterSingleton<IMvxFragmentsPresenter>(customPresenter);
return customPresenter;
}
ShellPage.cs
public class ShellPage : MvxCachingFragmentCompatActivity<ShellPageViewModel>, IMvxFragmentHost
{
.
.
.
public bool Show(MvxViewModelRequest request, Bundle bundle)
{
if (request.ViewModelType == typeof(MenuContentViewModel))
{
ShowFragment(request.ViewModelType.Name, Resource.Id.navigation_frame, bundle);
return true;
}
else
{
ShowFragment(request.ViewModelType.Name, Resource.Id.content_frame, bundle, true);
return true;
}
}
public bool Close(IMvxViewModel viewModel)
{
CloseFragment(viewModel.GetType().Name, Resource.Id.content_frame);
return true;
}
.
.
.
}
How can I achieve the same behavior in a Windows UWP app? Or rather, is there ANY example that exists for a Windows MvvmCross app which implements a CustomPresenter? That may at least give me a start as to how to implement it.
Thanks!
UPDATE:
I'm finally starting to figure out how to go about this with a customer presenter:
public class CustomPresenter : IMvxWindowsViewPresenter
{
IMvxWindowsFrame _rootFrame;
public CustomPresenter(IMvxWindowsFrame rootFrame)
{
_rootFrame = rootFrame;
}
public void AddPresentationHintHandler<THint>(Func<THint, bool> action) where THint : MvxPresentationHint
{
throw new NotImplementedException();
}
public void ChangePresentation(MvxPresentationHint hint)
{
throw new NotImplementedException();
}
public void Show(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ShellPageViewModel))
{
//_rootFrame?.Navigate(typeof(ShellPage), null); // throws an exception
((Frame)_rootFrame.UnderlyingControl).Content = new ShellPage();
}
}
}
When I try to do a navigation to the ShellPage, it fails. So when I set the Content to the ShellPage it works, but the ShellPage's ViewModel is not initialized automatically when I do it that way. I'm guessing ViewModels are initialized in MvvmCross using OnNavigatedTo ???
I ran into the same issue, and built a custom presenter for UWP. It loans a couple of ideas from an Android sample I found somewhere, which uses fragments. The idea is as follows.
I have a container view which can contain multiple sub-views with their own ViewModels. So I want to be able to present multiple views within the container.
Note: I'm using MvvmCross 4.0.0-beta3
Presenter
using System;
using Cirrious.CrossCore;
using Cirrious.CrossCore.Exceptions;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Views;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.WinUniversal.Extensions;
namespace xxxxx.WinUniversal.Presenters
{
public class MvxWindowsMultiRegionViewPresenter
: MvxWindowsViewPresenter
{
private readonly IMvxWindowsFrame _rootFrame;
public MvxWindowsMultiRegionViewPresenter(IMvxWindowsFrame rootFrame)
: base(rootFrame)
{
_rootFrame = rootFrame;
}
public override async void Show(MvxViewModelRequest request)
{
var host = _rootFrame.Content as IMvxMultiRegionHost;
var view = CreateView(request);
if (host != null && view.HasRegionAttribute())
{
host.Show(view as MvxWindowsPage);
}
else
{
base.Show(request);
}
}
private static IMvxWindowsView CreateView(MvxViewModelRequest request)
{
var viewFinder = Mvx.Resolve<IMvxViewsContainer>();
var viewType = viewFinder.GetViewType(request.ViewModelType);
if (viewType == null)
throw new MvxException("View Type not found for " + request.ViewModelType);
// Create instance of view
var viewObject = Activator.CreateInstance(viewType);
if (viewObject == null)
throw new MvxException("View not loaded for " + viewType);
var view = viewObject as IMvxWindowsView;
if (view == null)
throw new MvxException("Loaded View is not a IMvxWindowsView " + viewType);
view.ViewModel = LoadViewModel(request);
return view;
}
private static IMvxViewModel LoadViewModel(MvxViewModelRequest request)
{
// Load the viewModel
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
return viewModelLoader.LoadViewModel(request, null);
}
}
}
IMvxMultiRegionHost
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
namespace xxxxx.WinUniversal.Presenters
{
public interface IMvxMultiRegionHost
{
void Show(MvxWindowsPage view);
void CloseViewModel(IMvxViewModel viewModel);
void CloseAll();
}
}
RegionAttribute
using System;
namespace xxxxx.WinUniversal.Presenters
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class RegionAttribute
: Attribute
{
public RegionAttribute(string regionName)
{
Name = regionName;
}
public string Name { get; private set; }
}
}
These are the three foundational classes you need. Next you'll need to implement the IMvxMultiRegionHost in a MvxWindowsPage derived class.
This is the one I'm using:
HomeView.xaml.cs
using System;
using System.Diagnostics;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.Shared.Controls;
using xxxxx.WinUniversal.Extensions;
using xxxxx.WinUniversal.Presenters;
using xxxxx.Core.ViewModels;
namespace xxxxx.WinUniversal.Views
{
public partial class HomeView
: MvxWindowsPage
, IMvxMultiRegionHost
{
public HomeView()
{
InitializeComponent();
}
// ...
public void Show(MvxWindowsPage view)
{
if (!view.HasRegionAttribute())
throw new InvalidOperationException(
"View was expected to have a RegionAttribute, but none was specified.");
var regionName = view.GetRegionName();
RootSplitView.Content = view;
}
public void CloseViewModel(IMvxViewModel viewModel)
{
throw new NotImplementedException();
}
public void CloseAll()
{
throw new NotImplementedException();
}
}
}
The last piece to make this work is the way the actual xaml in the view is set-up. You'll notice that I'm using a SplitView control, and that I'm replacing the Content property with the new View that's coming in in the ShowView method on the HomeView class.
HomeView.xaml
<SplitView x:Name="RootSplitView"
DisplayMode="CompactInline"
IsPaneOpen="false"
CompactPaneLength="48"
OpenPaneLength="200">
<SplitView.Pane>
// Some ListView with menu items.
</SplitView.Pane>
<SplitView.Content>
// Initial content..
</SplitView.Content>
</SplitView>
EDIT:
Extension Methods
I forgot to post the two extension methods to determine if the view declares a [Region] attribute.
public static class RegionAttributeExtentionMethods
{
public static bool HasRegionAttribute(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
return attributes.Any();
}
public static string GetRegionName(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
if (!attributes.Any())
throw new InvalidOperationException("The IMvxView has no region attribute.");
return ((RegionAttribute)attributes.First()).Name;
}
}
Hope this helps.
As the link to the blog of #Stephanvs is no longer active I was able to pull the content off the Web Archive, i'll post it here for who ever is looking for it:
Implementing a Multi Region Presenter for Windows 10 UWP and MvvmCross
18 October 2015 on MvvmCross, Xamarin, UWP, Windows 10, Presenter > Universal Windows Platform
I'm upgrading a Windows Store app to the new Windows 10 Universal
Windows Platform. MvvmCross has added support for UWP in v4.0-beta2.
A new control in the UWP is the SplitView control. Basically it
functions as a container view which consist of two sub views, shown
side-by-side. Mostly it's used to implement the (in)famous hamburger
menu.
By default MvvmCross doesn't know how to deal with the SplitView, and
just replaces the entire screen contents with a new View when
navigating between ViewModels. If however we want to lay-out our views
differently and show multiple views within one window, we need a
different solution. Luckily we can plug-in a custom presenter, which
will take care of handling the lay-out per platform.
Registering the MultiRegionPresenter
In the Setup.cs file in your UWP project, you can override the
CreateViewPresenter method with the following implementation.
protected override IMvxWindowsViewPresenter CreateViewPresenter(IMvxWindowsFrame rootFrame)
{
return new MvxWindowsMultiRegionViewPresenter(rootFrame);
}
Using Regions
We can define a region by declaring a
element. At this point it has to be a Frame type because then we can
also show a nice transition animation when switching views.
<mvx:MvxWindowsPage ...>
<Grid>
<!-- ... -->
<SplitView>
<SplitView.Pane>
<!-- Menu Content as ListView or something similar -->
</SplitView.Pane>
<SplitView.Content>
<Frame x:Name="MainContent" />
</SplitView.Content>
</SplitView>
</Grid>
</mvx:MvxWindowsPage>
Now we want to be able when a ShowViewModel(...) occurs to swap out
the current view presented in the MainContent frame.
Showing Views in a Region
In the code-behind for a View we can now declare a MvxRegionAttribute,
defining in which region we want this View to be rendered. This name
has to match a Frame element in the view.
[MvxRegion("MainContent")]
public partial class PersonView
{
// ...
}
It's also possible to declare multiple regions within the same view.
This would allow you to split up your UI in more re-usable pieces.
Animating the Transition between Content Views
If you want a nice animation when transitioning between views in the
Frame, you can add the following snippet to the Frame declaration.
<Frame x:Name="MainContent">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<NavigationThemeTransition.DefaultNavigationTransitionInfo>
<EntranceNavigationTransitionInfo />
</NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
The contents will now be nicely animated when navigating.
Hope this helps, Stephanvs

Resources