Xamarin.Forms PCL MVVM Light > Custom Control > Best Practice?

785 views Asked by At

Hy

I would like to share my approach for a custom xamarin.forms control within a Xamarin PCL Project with MVVM-Light. What do you think about it?

Custom Control -> PersonPanel.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="xxx.PersonPanel">
  <StackLayout Orientation="Vertical">
    <Label x:Name="titleLabel" Text="{Binding TitleLabel}"/>
    <Entry x:Name="filterText" Placeholder="{Binding FilterPlaceholderText}" Text="{Binding Filter.Lookup}" TextChanged="OnFilterTextChanged"/>
    <Label x:Name="resultText" Text="{Binding ResultText}" IsVisible="{Binding ResultTextVisible}"/>
  </StackLayout>
</ContentView>

Code-Behind -> PersonPanel.xaml.cs:

public partial class PersonPanel : ContentView
{
    public PersonPanel()
    {
        InitializeComponent();

        //Init ViewModel
        BindingContext = ServiceLocator.Current.GetInstance<PersonPanelViewModel>();
    }

    private PersonPanelViewModel PersonPanelViewModel
    {
        get
        {
            return (PersonPanelViewModel)BindingContext;
        }
    }

    public string TitleLabel
    {
        get
        {
            return PersonPanelViewModel.TitleLabel;
        }
        set
        {
            PersonPanelViewModel.TitleLabel = value;
        }
    }

    public string FilterPlaceholderText
    {
        get
        {
            return PersonPanelViewModel.FilterPlaceholderText;
        }
        set
        {
            PersonPanelViewModel.FilterPlaceholderText = value;
        }
    }

    private void OnFilterTextChanged(object sender, EventArgs e)
    {
        PersonPanelViewModel.SearchCommand.Execute(null);
    }
}

ViewModel -> PersonPanelViewModel:

public class PersonPanelViewModel : ViewModelBase
{
    private IPersonService _personService;

    private decimal _personId = 0;
    private string _titleLabel = string.Empty;
    private string _filterPlaceholderText = string.Empty;
    private string _resultText = string.Empty;
    private bool _resultTextVisible = true;

    public PersonPanelViewModel(IPersonService personService)
    {
        _personService = personService;

        // Init Filter
        Filter = new PersonFilter();

        // Init Commands
        SearchCommand = new RelayCommand(Search);
    }

    public ICommand SearchCommand { get; set; }

    public PersonFilter Filter
    {
        get;
        private set;
    }

    public string ResultText
    {
        get
        {
            return _resultText;
        }
        set
        {
            Set(() => ResultText, ref _resultText, value);
        }
    }

    public bool ResultTextVisible
    {
        get
        {
            return _resultTextVisible;
        }
        set
        {
            Set(() => ResultTextVisible, ref _resultTextVisible, value);
        }
    }

    public string FilterPlaceholderText
    {
        get
        {
            return _filterPlaceholderText;
        }
        set
        {
            Set(() => FilterPlaceholderText, ref _filterPlaceholderText, value);
        }
    }

    public string TitleLabel
    {
        get
        {
            return _titleLabel;
        }
        set
        {
            Set(() => TitleLabel, ref _titleLabel, value);
        }
    }

    public decimal PersonId
    {
        get
        {
            return _PersonId;
        }
        set
        {
            Set(() => PersonId, ref _PersonId, value);
        }
    }

    private async void Search()
    {
        //Reset
        ResultText = string.Empty;
        ResultTextVisible = false;
        PersonId = 0;

        if (Filter.PersonLookup != null && Filter.PersonLookup.Length >= 3)
        {
            //Call to Person Service
            List<PersonResult> Person = await _personService.FindpersonByFilter(Filter);               

            if (Person.Count == 1)
            {
                PersonId = Person[0].PersonId;

                ResultText = Person[0].PersonName;
                ResultTextVisible = true;
            }
        }
    }
}

Using of Control in another View:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:components="clr-namespace:xxx.ViewElements.Components"
             x:Class="xxx.MainPage">
  <StackLayout Orientation="Vertical">
    <components:PersonPanel x:Name="personPanel" TitleLabel="Person" FilterPlaceholderText="Type your search criteria here..."/>
  </StackLayout>
</ContentPage>

I'm using Autofac as the IOC Container.

What do you think about it? I am using MVVM the right way (it's very new to me)?

Is there a better way to deal with calling the Command from the Event (TextChanged) on the view?

What's about the properties in the Code-Behind (which do a routing to the ViewModel)?

Edit: I'll try to describe, what I want to achieve:

  • Creating our own control (reusable in different views, cross-platform) -> PersonPanel.xaml
  • Written in XAML in our PCL Project with pure Xamarin.Forms controls in it
  • The control should have it's own commands (Search) and properties
  • One of the commands is using a Service
  • The control should get the Service (as an Interface) injected through IOC
  • The Service itself is also implemented in the PCL Project and makes a REST Call to a Webservice
  • The Result of the Call is than set to a property of the control -> ResultText Property
  • The Result is visible to the User

-> With the above implementation, all of that works, but I'm not sure if this is the right way...

Thanks for your help!

Kind regards, Peter

1

There are 1 answers

2
JordanMazurke On

The approach for mapping the event to the command is exactly as I would perform.

The rest is a little bit confusing. The general pattern is to create bindable properties in your control that are exposed to the view model when instantiated within the host view. A very basic sample structure is below:

public class TestLabelControl : Label
{
    public static readonly BindableProperty TestTitleProperty = BindableProperty.Create< TestLabelControl, string> (p => p.TestTitle, null);

    public string TestTitle {
        get {
            return (object)GetValue (TestTitleProperty);
        }
        set {
            SetValue (TestTitleProperty, value);
        }
    }
}

public class TestContentPage : ContentPage
{
    public TestContentPage()
    {
        var testLabel = new TestLabel();
        testLabel.SetBinding<TestContentPageViewModel>(TestLabel.TestTitleProperty, vm => vm.TestLabelTitle, BindingMode.Default);
        Content = testLabel;
    }
}


public class TestContentPageViewModel
{
    public string TestLabelTitle{get;set;}

    public TestContentPageViewModel()
    {
        TestLabelTitle = "Something random";
    }
}

You would then create the native renderers to handle the drawing on each platform.

By following this approach you keep the code separated and concise. It does seem a slightly long winded way of getting things done but it is highly scalable and configurable.