Hello, friends. When building Xamarin.Forms applications, you can call extremely simple dialogs to either display information or allow the user to select options only, and not complex dialogs that allow the addition of entries, input views and other complex views. At least not without additional external plugins else to my knowledge yet. This is not so ideal, since one may need to call simple dialogs to perform simple tasks like getting numbers, or strings or any type of input from the user. And do so without much stress. What we will be doing in this tutorial is just that. Calling native dialogs and passing data back to the shared code while respecting MVVM.
The app which we will be building is a simple application which will permit users to create to do items and mark them as completed, the creation of these todo items will be done with dialogs, and when they are created, the underlying view will be populated with the newly created item. Let's dive into the code.
public interface ICallDialog
{
Task CallDialog(object viewModel);
}
public class MainViewModel : ReactiveObject
{
ReactiveList<Todo> _todos;
public ReactiveList<Todo> Todos
{
get => _todos;
set => this.RaiseAndSetIfChanged(ref _todos, value);
}
public ReactiveCommand CreateTodoCommand { get; set; }
public MainViewModel()
{
CreateTodoCommand = ReactiveCommand.Create(async () =>
{
//CAll the Dialogs
await DependencyService.Get<ICallDialog>().CallDialog(new CreateTodoViewModel());
});
Todos = new ReactiveList<Todo>() { ChangeTrackingEnabled = true };
//Observe when the todo's ISdone property is
//set to true, perform an action.
Todos.ItemChanged.Where(x => x.PropertyName == "IsDone" && x.Sender.IsDone)
.Select(x => x.Sender)
.Subscribe(x =>
{
if (x.IsDone)
{
Todos.Remove(x);
Todos.Add(x);
}
});
}
public void Initialize()
{
MessagingCenter.Subscribe<object, Todo>(this, $"ItemCreated", (s, todo) =>
{
Todos.Add(todo);
});
}
public void Stop()
{
MessagingCenter.Unsubscribe<object, Todo>(this, $"ItemCreated");
}
}
public sealed partial class CreateTodoDialog : ContentDialog
{
//Call reference the Viewmodel
CreateTodoViewModel ViewModel => DataContext as CreateTodoViewModel;
bool _canClose;
public CreateTodoDialog(CreateTodoViewModel vm)
{
this.InitializeComponent();
//Set the Datacontext to the Viewmodel
DataContext = vm;
Closing += CreateCategoryDialog_Closing;
}
private void CreateCategoryDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args)
{
if (!_canClose)
{
args.Cancel = true;
}
}
///
/// This permits teh addition of validation in case the user
/// does not fill the Title
///
///
///
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
//Perform slight validation in case the Title was input
if(string.IsNullOrEmpty(TitleDialog.Text))
{
TitleDialog.BorderBrush = new SolidColorBrush(Colors.Red);
}
else
{
if((ViewModel.CreateTodo as ICommand).CanExecute(null))
{
(ViewModel.CreateTodo as ICommand).Execute(null);
}
_canClose = true;
this.Hide();
}
}
private void ContentDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
_canClose = true;
this.Hide();
}
}
<ContentDialog
x:Class="NativeCustomDialogs.UWP.Dialogs.CreateTodoDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:NativeCustomDialogs.UWP.Dialogs"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="New Todo"
PrimaryButtonText="Ok"
SecondaryButtonText="Cancel"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
SecondaryButtonClick="ContentDialog_SecondaryButtonClick">
<Grid>
<TextBox Text="{Binding Title, Mode=TwoWay}" Name="TitleDialog"/>
</Grid>
</ContentDialog>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/TitleEditText"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_width="match_parent" />
<LinearLayout
android:layout_margin="10dp"
android:orientation="horizontal"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<Button
android:layout_gravity="center"
android:id="@+id/CatDoneButton"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<Button
android:layout_gravity="center"
android:id="@+id/CatCancelButton"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
</LinearLayout>
</LinearLayout>
public class CreateTodoDialog : ReactiveDialogFragment
{
public CreateTodoViewModel ViewModel { get; set; }
EditText _titleEditText;
Button _doneBtn;
Button _cancelBtn;
Spinner _icons;
public CreateTodoDialog(CreateTodoViewModel vm)
{
ViewModel = vm;
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.CreateTodoLayout,
container, false);
_titleEditText = view.FindViewById<EditText>(Resource.Id.TitleEditText);
_doneBtn = view.FindViewById<Button>(Resource.Id.CatDoneButton);
_cancelBtn = view.FindViewById<Button>(Resource.Id.CatCancelButton);
_titleEditText.Hint = "Title";
//Use WhenAny to create a kind of one way databining between view and viewmodel property
this.WhenAny(x => x._titleEditText.Text, x => x.Value).Subscribe((val) =>
{
ViewModel.Title = val;
});
_doneBtn.Text = "Done";
_cancelBtn.Text = "Cancel";
_doneBtn.Click += DoneBtn_Click;
_cancelBtn.Click += (o, e) => this.Dismiss();
return view;
}
private async void DoneBtn_Click(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(_titleEditText.Text))
{
if (((ICommand)ViewModel.CreateTodo).CanExecute(null))
{
((ICommand)ViewModel.CreateTodo).Execute(null);
this.Dismiss();
}
}
else
{
_titleEditText.SetError("Enter the title please", Resources.GetDrawable(Resource.Drawable.abc_ab_share_pack_mtrl_alpha));
}
}
public override Dialog OnCreateDialog(Bundle savedState)
{
var dialog = base.OnCreateDialog(savedState);
dialog.SetTitle("Create New Todo");
return dialog;
}
}
public class CreateTodoViewModel : ReactiveObject
{
public ReactiveCommand CreateTodo { get; set; }
private string _title;
public string Title
{
get { return _title; }
set {
this.RaiseAndSetIfChanged(ref _title, value); }
}
public CreateTodoViewModel()
{
CreateTodo = ReactiveCommand.Create(() =>
{
///When the Item's creation is done, signal
///Any object listenning for this message that this creation
///is completed and pass it the Created object
MessagingCenter.Send<object, Todo>(this, $"ItemCreated", new Todo { Title = Title, IsDone = false});
});
}
}
}
[assembly: Xamarin.Forms.Dependency(
typeof(CallDialog))]
namespace NativeCustomDialogs.Droid
{
public class CallDialog : ICallDialog
{
async Task ICallDialog.CallDialog(object viewModel)
{
var activity = CrossCurrentActivity.Current.Activity as FormsAppCompatActivity;
new CreateTodoDialog(viewModel as CreateTodoViewModel)
.Show(activity.SupportFragmentManager, "CreateTodoDialog");
}
}
[assembly: Xamarin.Forms.Dependency(
typeof(CallDialog))]
namespace NativeCustomDialogs.UWP
{
public class CallDialog : ICallDialog
{
async Task ICallDialog.CallDialog(object viewModel)
{
await new CreateTodoDialog(viewModel as CreateTodoViewModel).ShowAsync();
}
}
}
With this, the code for calling the dialogs should be functioning perfectly. And from this simple demonstration, other compexe dialogs could be called easily using the same technique described here. For any issues, you can get to this github repo
](url)