1
votes

Problem

Hi! I would like to implement a custom UI component with some basic logic for Android in MvvmCross so it would be reusable across different layouts. What we were doing so far is using a BaseActivity with the components that would be reused and inheriting from it (for example, for the toolbar). Some time later, the need to apply different toolbars in some activities appeared, so we had two choices there:

  • Put more than one toolbar inside the AppBarLayout in the BaseActivity and each activity controller which inherits from BaseActivity would set the visibility for the toolbar it would want to use (including use more than one toolbar, if necessary).
  • Make another parent class (something like BaseActivityWithOtherHeader) for each different combination of headers and each activity would inherit from the corresponding BaseActivity.

We took the first approach, but neither of them would be maintanable for much time, for obvious reasons...

Hypothesis

So, from my perception, the best approach for this is to create an .axml file in which I specify the layout of the component and (maybe) create a ViewController in which I apply Fluent Binding to the ViewModel containing the logic of said component. This way, the logic is shared across different platforms. Also, if I'm not mistaken, this approach would let me an my peers include it in our Activities by simply using the <include> tag (and perhaps just a declaration on the ViewController of the Activity instead of the whole logic, or something like that), which would comply with the Don't Repeat Yourself paradigm, providing a cleaner code and faster development.

But I couldn't find any examples nor guidelines for implementing Reusable Components with MvvmCross, either by googling or in the documentation...

What I found was only this question, which refers to and old version of MvvmCross, and apparently there is no more an IMvxLayoutInflater class, nor does theIMvxBindingContext object has the BindingInflate() method...

Questions

So, I have two questions regarding this matter:

  • Is there a better approach for this than the proposed above, considering I am using MvvmCross 6.2.1?
  • Could you provide any examples for creating any custom reusable component with MvvmCross 6.x, be it with the proposed method or another one not mentioned in the first section?
1

1 Answers

1
votes

Yes, that way you say will work and you could reuse them but take into account that because of the axml you won't be able to change the bindings that you have there. So if you have a ViewModel that you reuse also then you have no problem.

In case you want to change the bindings of the control then you'll need to create a control (.cs) where you add the inner views programmatically or inflate your axml, do the logic and have properties to bind to so that you can reuse that control and bind it.

E.g.:

[Register("myNamespace.SwitchRightText")]
public class SwitchRightText : LinearLayout
{
    public SwitchRightText(Context context, IAttributeSet attrs, int defStyleAttr)
        : base(context, attrs, defStyleAttr)
    {
        this.Init(context, attrs, defStyleAttr);
    }

    public SwitchRightText(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes)
        : base(context, attrs, defStyleAttr, defStyleRes)
    {
        this.Init(context, attrs, defStyleAttr, defStyleRes);
    }

    public SwitchRightText(Context context, IAttributeSet attrs)
        : base(context, attrs)
    {
        this.Init(context, attrs);
    }

    public SwitchRightText(Context context)
        : base(context)
    {
        this.Init(context);
    }

    public SwitchRightText(IntPtr javaReference, JniHandleOwnership transfer)
        : base(javaReference, transfer)
    {
    }

    // Controls
    public SwitchCompat Switch { get; set; }

    public TextView TextView { get; set; }

    private bool _toggleBackgrond;
    public bool ToggleBackground
    {
        get => this._toggleBackgrond;
        set
        {
            this._toggleBackgrond = value;
            this.UpdateBackground();
        }
    }

    // Methods
    private void Init(Context context = null, IAttributeSet attrs = null, int defStyleAttr = 0, int defStyleRes = 0)
    {
        if(IsInEditMode)
            return;

        this.Orientation = Orientation.Horizontal;

        this.InitializeSwitch(context, attrs);
        this.InitializeTextView(context, attrs);

        this.AddView(this.Switch);
        this.AddView(this.TextView);
    }

    private void InitializeSwitch(Context context, IAttributeSet attrs = null)
    {
        this.Switch = new SwitchCompat(context, attrs);
        this.Switch.ShowText = false;
    }

    private void InitializeTextView(Context context, IAttributeSet attrs = null)
    {
        this.TextView = new TextView(context, attrs);
    }

    private void UpdateBackground()
    {
        this.SetBackgroundColor(this.ToggleBackground ? Android.Graphics.Color.Black : Android.Graphics.Color.Yellow);
        this.Invalidate();
    }
}

You may need a custom binding e.g.

public class TextSwitchRightTextTargetBinding : MvxAndroidTargetBinding
{
    public TextSwitchRightTextTargetBinding(SwitchRightText switchRightText)
        : base(switchRightText)
    {
    }

    private SwitchRightText SwitchRightText => (SwitchRightText)this.Target;

    protected override void SetValueImpl(object target, object value)
    {
        this.SwitchRightText.TextView.Text = (string)value;
    }

    public override Type TargetType => typeof(string);

    public override MvxBindingMode DefaultMode => MvxBindingMode.TwoWay;

    public override void SubscribeToEvents()
    {
        base.SubscribeToEvents();

        this.SwitchRightText.TextView.TextChanged += TextView_TextChanged;
    }

    private void TextView_TextChanged(object sender, Android.Text.TextChangedEventArgs e)
    {
        this.FireValueChanged(this.SwitchRightText.TextView.Text);
    }

    protected override void Dispose(bool isDisposing)
    {
        base.Dispose(isDisposing);

        if(isDisposing)
            this.SwitchRightText.TextView.TextChanged -= TextView_TextChanged;
    }
}

And register it in your Setup:

protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
    base.FillTargetFactories(registry);

    registry.RegisterFactory(new MvxCustomBindingFactory<SwitchRightText>("Text", srt => new TextSwitchRightTextTargetBinding(srt)));
}

Then in your .axml you can use it like:

<myNamespace.SwitchRightText
    ...
    mvxBind="Text MyTextInMyViewModel; ToggleBackground ToggleBackgroundInMyVM" />

In iOS you can either do a custom UIViewController or a custom UIView add it to wherever you want to use it and do the bindings.


Another way is to have a Shared project and do all the logic of the component there

FFImageLoading does that in the control MvxCachedImageView

Excerpt from the code

#if __IOS__
    [Preserve(AllMembers = true)]
    [Register("MvxCachedImageView")]
#elif __ANDROID__
    [Preserve(AllMembers = true)]
    [Register("ffimageloading.cross.MvxCachedImageView")]
#endif
    /// <summary>
    /// MvxCachedImageView by Daniel Luberda
    /// </summary>
    public class MvxCachedImageView
#if __IOS__
        : UIImageView, ICachedImageView, INotifyPropertyChanged
#elif __ANDROID__
        : ImageViewAsync, ICachedImageView, INotifyPropertyChanged
#endif