My Generic Approach to CRUD-Action-Page

ThomasBurkhartThomasBurkhart DEMember ✭✭✭✭
edited February 2016 in Xamarin.Forms

Hi,

I asked some time ago what's the best way to implement CRUD-Dialogs in a reusable way. I like now to show you my solution. Perhaps it might help some of you. I want to point out that I'm still an Xamarin beginner, so not everything might be the best way.

I use FreshMVVM as Framework, but this approach can be used with other MVVM Implementations.

To be able to use this Classes your Business-Objects have implement this Interface

    public interface  ICRUDObject
    {
        Task Save();
        Task Delete();
    }

The CRUD-Dialog has to inherit from CRUDBasePage:

    public class CRUDBasePage : ContentPage
    {
        //Todo Icons for other platforms
        public CRUDBasePage()
        {
            SetBinding(TitleProperty, new Binding("PageTitle"));

            //SaveIcon
            var newItem = new BindableToolbarItem
            {
                Text = Texts.Save,
                Order = ToolbarItemOrder.Primary,
                Priority = 0,
                Parent = this,
                Icon = "ic_action_save.png"
            };

            newItem.SetBinding(MenuItem.CommandProperty, new Binding("SaveCommand"));
            newItem.SetBinding(BindableToolbarItem.IsVisibleProperty, new Binding("IsChanged"));

            ToolbarItems.Add(newItem);

            //CancelButton
            newItem = new BindableToolbarItem
            {
                Text = Texts.Cancel,
                Order = ToolbarItemOrder.Primary,
                Priority = 4,
                Parent = this,
                Icon = "ic_action_cancel.png"
            };

            newItem.SetBinding(MenuItem.CommandProperty, new Binding("CancelCommand"));
            newItem.SetBinding(BindableToolbarItem.IsVisibleProperty, new Binding("CancelIconEnabled"));

            ToolbarItems.Add(newItem);

            //EditButton
            newItem = new BindableToolbarItem
            {
                Text = Texts.Edit,
                Order = ToolbarItemOrder.Primary,
                Priority = 2,
                Parent = this,
                Icon = "ic_action_edit.png"
            };

            newItem.SetBinding(MenuItem.CommandProperty, new Binding("EditCommand"));
            newItem.SetBinding(BindableToolbarItem.IsVisibleProperty, new Binding("EditIconEnabled"));

            ToolbarItems.Add(newItem);

            //DeleteButton
            newItem = new BindableToolbarItem
            {
                Text = Texts.Delete,
                Order = ToolbarItemOrder.Primary,
                Priority = 3,
                Parent = this,
                Icon = "ic_action_discard.png"
            };

            newItem.SetBinding(MenuItem.CommandProperty, new Binding("DeleteCommand"));
            newItem.SetBinding(BindableToolbarItem.IsVisibleProperty, new Binding("DeleteIconEnabled"));

            ToolbarItems.Add(newItem);
        }


        protected override bool OnBackButtonPressed()
        {
            var model  = (CRUDBasePageModel)  this.GetModel();


            if (model.OnBackButtonPressed())
            {
                return base.OnBackButtonPressed();
            }
            return false;
        }
    }

This handles the correct handling of Icons in the Actionbar, meaning showing or hiding buttons in the right context.

To make this work your linked PageModels have to Inherit from CRUDBasePageModel

    [ImplementPropertyChanged]
    public abstract class CRUDBasePageModel : FreshBasePageModel
    {


        protected ICRUDObject TheCRUDObject { get; set; }


        public String PageTitle { get; set; }

        public bool CancelIconEnabled { get; set; }
        public bool DeleteIconEnabled { get; set; }
        public bool EditIconEnabled { get; set; }


        //Will be automatically set bei Fody if any Property has changed
        // Has to be manually reset
        public bool IsChanged { get; set; }

        public bool EditMode { get; set; }

        protected bool NewItem;


        public Command SaveCommand { get; protected set; }
        public Command EditCommand { get; protected set; }
        public Command DeleteCommand { get; protected set; }
        public Command CancelCommand { get; protected set; }

        protected CRUDBasePageModel()
        {
            CancelCommand = new Command(Cancel);
            SaveCommand = new Command(Save);
            EditCommand = new Command(Edit);
            DeleteCommand = new Command(Delete);
        }


        private void Edit()
        {
            EditIconEnabled = false;
            EditMode = true;
            IsChanged = false;
        }


        //This is the FreshMVVM way to transfer data to a newly pushed PageModel
        public override void Init(object initData)
        {
            base.Init(initData);

            if (initData == null)
            {
                NewItem = true;
                TheCRUDObject = CreateNewCRUDObject();
            }
            else
            {
                NewItem = false;
                TheCRUDObject = (ICRUDObject)initData;

                AutoMapper.Mapper.Map(TheCRUDObject, this);
            }
        }

        public virtual async void Cancel()
        {
            if (IsChanged)
            {
                if (!await UserDialogs.Instance.ConfirmAsync(Texts.DataNotStored, Texts.Warning, 
                                                             Texts.Continue, Texts.Cancel))
                {
                    return;
                }
            }
            await CoreMethods.PopPageModel(true);
        }


        public virtual async void Delete()
        {
            if (await UserDialogs.Instance.ConfirmAsync(Texts.ReallyDelete, Texts.Warning, Texts.Continue,
                                                         Texts.Cancel))
            {
                await TheCRUDObject.Delete();
            }

            await CoreMethods.PopPageModel(true);
        }


        public virtual async void Save()
        {
            if (! await CanSave())
            {
                return;
            }

            AutoMapper.Mapper.Map(this, TheCRUDObject);

            var dlg = Acr.UserDialogs.UserDialogs.Instance.Progress(Texts.PleaseWait);

            await TheCRUDObject.Save();

            dlg.Dispose();

            await CoreMethods.PopPageModel(true);
        }


        public virtual bool OnBackButtonPressed()
        {
            if (IsChanged)
            {

                Task<bool> confirm = UserDialogs.Instance.ConfirmAsync(Texts.DataNotStored, Texts.Warning,
                                                                       Texts.Continue,
                                                                       Texts.Cancel);
                confirm.ContinueWith(task =>
                {
                    if (task.Result)
                    {
                        CoreMethods.PopPageModel(true);
                    }
                });
            }
            return false;
        }

        protected override void ViewIsAppearing(object sender, EventArgs e)
        {
            //Strangely if you don't change the value of this properties twice the first time the MenubarIcons don't get updated
            if (NewItem)
            {
                EditIconEnabled = true;
                EditIconEnabled = false;
                DeleteIconEnabled = true;
                DeleteIconEnabled = false;
                EditMode = true;

            }
            else
            {
                EditIconEnabled = true;
                DeleteIconEnabled = true;
                EditMode = false;
            }

            IsChanged = true;
            IsChanged = false;


        }

        protected override void ViewIsDisappearing(object sender, EventArgs e)
        {
            KeyboardHelper.HideKeyboard();
            base.ViewIsDisappearing(sender, e);
        }


        // Functions to be overrirden by the concrete ViewModel

        //Creates a real CRUDObject. Is called when NewButton is pressed
        protected abstract ICRUDObject CreateNewCRUDObject();

        //Is called before saving an Icon. 
        protected virtual async Task<bool> CanSave()
        {
            return true;
        }


    }

By using Fody and Automapper this class handles all CRUD operations. The Properties to display/edit only have to have the same Names as the Properties of your CRUDObject and Automapper takes care of Updating in both ways (Cancel case is handled)

The real PageModel looks pretty simple now:

<br />    [ImplementPropertyChanged]
    public class IngredientPageModel : CRUDBasePageModel
    {

       //Properties matching business object with ICRUDObject
        public string Name { get; set; }
        public string Description { get; set; }


        protected override ICRUDObject CreateNewCRUDObject()
        {
            return new Ingredient
            {
                Name = Name,
                Description = Description,
                UUID = Guid.NewGuid().ToString("D")
            };
        }


        protected override async Task<bool> CanSave()
        {
            if ((Name == null) || (Name.Length == 0))
            {
                UserDialogs.Instance.Alert(Texts.EmptyName);
                return false;
            }
            if (!EditMode && (await StorageManager.Instance.IngredientNameExist(Name)))
            {
                UserDialogs.Instance.Alert(Texts.NameAlreadyExists);
                return false;
            }
            return true;
        }
    }

And here the derived page:

Code behind:

public partial class IngredientPage
{

    public IngredientPage ()
    {
                InitializeComponent();
        }

    }   

And XAML

<?xml version="1.0" encoding="utf-8"?>

<pages:CRUDBasePage  xmlns="http://xamarin.com/schemas/2014/forms"
                          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                          xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
                          xmlns:local="clr-namespace:BREADy;assembly=EmbeddedResources"
                          xmlns:pages="clr-namespace:BREADy.Pages;assembly=BREADy"
                          x:Class="BREADy.Pages.IngredientPage">

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="20, 40, 20, 20"
                    Android="20, 20, 20, 20"
                    WinPhone="20, 20, 20, 20" />
    </ContentPage.Padding>

    <ContentPage.Resources>
        <ResourceDictionary>
            <local:BooleanNegationConverter x:Key="not" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <ContentPage.Content>
        <ScrollView Orientation="Vertical">
            <StackLayout Orientation="Vertical">
                <BoxView Color="Transparent" HeightRequest="10" />
                <BoxView Color="Gray" HeightRequest="1" />
                <BoxView Color="Transparent" HeightRequest="10" />

                <Label Text="{local:Translate Name}" FontSize="Medium" />
                <Entry Text="{Binding Name}" FontSize="Medium"
                       IsVisible="{Binding EditMode}"/>
                <Label Text="{Binding Name}" FontSize="Medium"
                       IsVisible="{Binding EditMode,Converter={StaticResource not}}"/>


                <BoxView Color="Transparent" HeightRequest="20" />

                <Label Text="{local:Translate Description}" FontSize="Medium" />

                <Editor Text="{Binding Description}" FontSize="Medium"
                        IsVisible="{Binding EditMode}"/>
                <Label Text="{Binding Description}" FontSize="Medium"
                       LineBreakMode="WordWrap"
                       IsVisible="{Binding EditMode,Converter={StaticResource not}}"/>
            </StackLayout>
        </ScrollView>
    </ContentPage.Content>


</pages:CRUDBasePage>

To have the Actionbar be able to bind the visibility of the icons you have to use this Version from @AdamP thanks!

<br />    public class BindableToolbarItem : ToolbarItem
    {
        public static BindableProperty IsVisibleProperty =
            BindableProperty.Create<BindableToolbarItem, bool>(o => o.IsVisible, false,
                propertyChanged: OnIsVisibleChanged);

        public BindableToolbarItem()
        {
            InitVisibility();
        }

        public new ContentPage Parent { set; get; }

        public bool IsVisible
        {
            get { return (bool) GetValue(IsVisibleProperty); }
            set { SetValue(IsVisibleProperty, value); }
        }

        private void InitVisibility()
        {
            IsVisible = true;
            OnIsVisibleChanged(this, false, IsVisible);
        }

        private static void OnIsVisibleChanged(BindableObject bindable, bool oldvalue, bool newvalue)
        {
            var item = bindable as BindableToolbarItem;

            if (item.Parent == null)
                return;

            var items = item.Parent.ToolbarItems;

            if (newvalue && !items.Contains(item))
            {
                items.Add(item);
            }
            else if (!newvalue && items.Contains(item))
            {
                items.Remove(item);
            }
        }
    }

And here the VaueConverter to invert bool bindings

    public class BooleanNegationConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool) value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool) value;
        }
    }

Have fun!

Tagged:

Posts

Sign In or Register to comment.