Calling Custom Input Native Dialogs in an MVVM Xamarin.Forms Application

This tutorial will teach you how to call native custom Input dialogs in Xamarin.Forms

  • Create Custom Native Dialogs per platform with MVVM and ReactiveUI
  • Calling the Dialogs in the shared Xamarin.Forms application
  • Receive data from the dialogs.

You will need the following Nuget packages to your Xamarin.Forms application

Difficulty

  • Intermediate

Let's dive in

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.

  • Create the View model for the mainpage in your shared code.
  • Create an interface which will be used to call the dialogs as follows
    public interface ICallDialog
    {
        Task CallDialog(object viewModel);
    }
  • We do this cause we will use the Dependency service to call these dialogs per platform
  • Here is the code for the Main View Model:
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");
        }
    }
  • Create the Dialogs per platform, in this guide, the platforms covered are Android and UWP, as shown below.
  • Create a viewmodel property which will be instantiated in the constructor of the dialog, and set as the dialog's data context.

UWP Code behind

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();
        }
    }

UWP Xaml


<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>

  • Let's create the Dialog on Android,
  • In your Layout folder, create a layout for the dialog here is mine:
<?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>

  • Create a new class and make it inherit from ReactiveDialogFragment
  • We do this because we will be using the ReactiveDialogFragment's methods to implement a kind of custom data binding to the Dialog's ViewModel Properties in android.
  • Here is the code for this :
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;
        }
    }
  • These dialogs will have one ViewModel in the shared library, no matter which platform they are on.
  • Here is the code for this ViewModel:
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});
            });
        }
    }
}
  • Implement the ICallDialog interface in each platform
  • In Android, use the CrossCurrentActivity Plugin to get the current activity as follows:
[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");
        }
    }
  • On UWP :
[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

UWPDemo2.gif
](url)



Posted on Utopian.io - Rewarding Open Source Contributors

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now
Logo
Center