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

in utopian-io •  7 years ago  (edited)

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

        /// <summary>
        /// This permits teh addition of validation in case the user 
        /// does not fill the Title
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        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

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for your contribution.
While I appreciate the effort, yet it cannot be accepted for reasons below:

  • You did not include enough description, details, or comments within your code to reflect what the different sections are supposed to perform. Well commented and explained code can mean the difference between a good and a malformed tutorial
  • The github repo you linked contains some code portions which are much larger, and at times different than what you pasted in this contribution, please make sure to only reference code that makes sense for your reader
  • I would also advise to include screenshots as to the outcome of running your code/tutorial, and what the reader would expect to see.
  • Also please make sure you select the github repository relevant to xamarin forms, and NOT to your code itself, as the contribution is about xamarin forms.
    Looking forward to your next tutorial !

Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.

[utopian-moderator]

Ok, I don't understand everything you say.

  • The repo contains code which is much larger cause I pasted only the most relevant code in the tutorial. Because I want to show code which is most useful for my audience. as you said and the point you mentioned here is contradictory.
  • I added a GIF showing the complete working demo of the code a while ago, Check the post again.
  • I just changed the github repository to xamarin.forms official repository as you said.
  • My code is commented where need be and before I paste any line of code I explain what the code is for and why is it that I put it there please read through the tutorial again.

Congratulations @damiendoumer! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

You got your First payout

Click on any badge to view your own Board of Honor on SteemitBoard.
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

Upvote this notification to help all Steemit users. Learn why here!