How do I create an alert popup from my ViewModel in Xamarin? - 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.

Related

Xamarin Forms Commands with Async Methods without lambda

I am trying to pass an asynchronous method to a command in xamarin forms. In microsoft docs, the sample codes are provided with lambda expressions. As I am pretty new at c#, I want to see the explicit form of it to understand the concept clearly:
The code with lambda:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
NavigateCommand = new Command<Type>(
async (Type pageType) =>
{
Page page = (Page)Activator.CreateInstance(pageType);
await Navigation.PushAsync(page);
});
BindingContext = this;
}
public ICommand NavigateCommand { private set; get; }
}
So, my question is how to retype NavigationCommand without lambda. I think It would be more beneficial to the beginners.
Thanks a lot for any respond.
You could check the following code
NavigateCommand = new Command<Type>((pageType) => TestCommand(pageType));
async void TestCommand(Type pageType)
{
Page page = (Page)Activator.CreateInstance(pageType);
await Navigation.PushAsync(page);
}
If your method has no argument, you could init it like
NavigateCommand = new Command(TestCommand);
async void TestCommand()
{
//...
}

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.

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

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.

How to access activity Argument within ActivityDesigner?

I need to get my custom activity's InArgument value at ActivityDesigner.
MyActivity:
[Designer(typeof(ReadTextDesigner))]
public sealed class ReadText : CodeActivity
{
public InArgument<string> ImageName { get; set; }
protected override void Execute(CodeActivityContext context)
{
}
}
My designer:
public partial class ReadTextDesigner
{
public ReadTextDesigner()
{
InitializeComponent();
//this.ModelItem is null here.. WHY is it null?
//How do I get Activity's ImageName here?
}
}
I also have a button as in the image below and when you click on it, I CAN SET my custom Activity's value like this:
private void BtnStart_OnClick(object sender, RoutedEventArgs e)
{
this.ModelItem?.Properties["ImageName"]?.SetValue(new InArgument<string>()
{
Expression = "some value"
});
}
XAML:
<sapv:ExpressionTextBox
Expression="{Binding Path=ModelItem.ImageName, Mode=TwoWay, Converter={StaticResource ArgumentToExpressionConverter}, ConverterParameter=In }"
ExpressionType="s:String"
HintText="Enter a string"
OwnerActivity="{Binding Path=ModelItem}"
Width="110"
Margin="0,5"
Grid.Row="0"
MaxLines="1"
x:Name="TxtImagePath"/>
<Button Grid.Column="0" Grid.Row="1" Content="Get Image" HorizontalAlignment="Center" Click="BtnStart_OnClick" x:Name="BtnStart"/>
How do I GET Activity InArgument ReadTextDesigner constructor?
Its quite weird but I found a workaround. Although this is A solution, I'm hoping for a much better one;
Since within the constructor, I cannot get the ModelItem, I created a new Thread apart from the Main Thread. This new Thread waits 2 milliseconds and then tries to get ModelItem and it succeeds somehow.
Here is the new modified ReadTextDesigner code (Note: I only changed ReadTextDesigner code nothing else)
public ReadTextDesigner()
{
InitializeComponent();
new TaskFactory().StartNew(() => { this.Dispatcher.Invoke(() => SetImage(this)); });
}
private void SetImage(ReadTextDesigner designer)
{
Thread.Sleep(2);
if (designer.ModelItem.GetCurrentValue() is ReadText readText)
{
var imageName = readText.ImageName?.Expression?.Convert<string>();
if (!string.IsNullOrWhiteSpace(imageName))
{
//imageName has a value at this point!
}
}
}
ModelItem is not null anymore and carries the necessary value.
Hope this will help someone or someone post a better solution.
Cheers!

How to send more than one param with DelegateCommnad

I have Button in ListView Cell. On button click I need to perform two actions from ViewModel
Get the current record (to make modification in data)
Get the current Cell/View (to modify text, bg color of button)
However, I am able to perform single action at a time using DelegateCommand by passing Student and object param respectively. See my code below
public StudentAttendanceListPageViewModel()
{
//BtnTextCommand=new DelegateCommand<object>(SetBtnText);
ItemCommand=new DelegateCommand<Student>(BtnClicked);
}
public DelegateCommand<object> BtnTextCommand { get; private set; }
public void SetBtnText(object sender)
{
if (view.Text == "P")
{
view.Text = "A";
view.BackgroundColor= (Color)Application.Current.Resources["lighRedAbsent"];
}
}
public DelegateCommand<Student> ItemCommand { get; }
public void BtnClicked(Student objStudent)
{
objStudent.AbsentReasonId="1001";
objStudent.AttendanceTypeStatusCD = "Absent";
objStudent.AttendanceTypeStatusId = "78001"
}
This is Button code
<Button x:Name="mybtn"
Command="{Binding Source={x:Reference ThePage}, Path=BindingContext.ItemCommand}"
CommandParameter="{Binding .}"
BackgroundColor="{DynamicResource CaribGreenPresent}"
Text="{Binding AttendanceTypeStatusId, Converter={x:StaticResource IDToStringConverter}}">
</Button>
If you see above code I have two methods SetBtnText and BtnClicked. How can I merge these two methods into one by passing Student and object params at a time in DelegateCommand?
You should bind the view's properties to the view model. Then pass the view model as command parameter and change whatever you want to change in the command and data binding will automatically update the view.
Example:
<Button Command="{Binding SomeCommand}"
Text="{Binding Text}">
</Button>
public class StudentViewModel
{
public StudentViewModel( Student student )
{
_text = $"Kick {student.Name}";
SomeCommand = new DelegateCommand( () => {
Text = "I have been kicked"
student.Exmatriculate();
SomeCommand.RaiseCanExecuteChanged();
},
() => student.IsMatriculated
);
}
public DelegateCommand SomeCommand { get; }
public string Text
{
get => _text;
set => SetProperty( ref _text, value );
}
private string _text;
}
As stated in the comments already, it's never ever necessary to pass the view to the view model. To me, it looks as if you don't have a view model in the first place, though, as your code only mentions Student (which most likely is part of the model), while there's no occurence of a StudentViewModel. You know, you do not bind to the model directly unless it's a trivial toy project.

Resources