MvvmCross UITextField custom binding

2019-02-24 20:26发布

问题:

So I am trying to implement a custom binding for a UITextField in MvvmCross, pretty much along the lines of Binding 'GO' key on Software Keyboard - i.e. trying to bind a text field to automatically fire an event when the Done button is tapped on the keyboard (so binding to ShouldReturn). I also need to bind the text field's EditingDidBegin and EditingDidEnd events. Because I am binding more than one event, I have created a MvxPropertyInfoTargetBinding as follows:

public class MyTextFieldTargetBinding : MvxPropertyInfoTargetBinding<UITextField>
{
    private ICommand _command;

    protected UITextField TextField
    {
        get { return (UITextField)Target; }
    }

    public MyTextFieldTargetBinding(object target, PropertyInfo targetPropertyInfo) : base(target, targetPropertyInfo)
    {
        TextField.ShouldReturn += HandleShouldReturn;
        TextField.EditingDidBegin += HandleEditingDidBegin;
        TextField.EditingDidEnd += HandleEditingDidEnd;
    }

    private bool HandleShouldReturn(UITextField textField)
    {
        if (_command == null) {
            return false;
        }

        var text = textField.Text;
        if (!_command.CanExecute (text)) {
            return false;
        }

        textField.ResignFirstResponder();
        _command.Execute(text);

        return true;
    }

    private void HandleEditingDidBegin (object sender, EventArgs e)
    {
        // do something
    }

    private void HandleEditingDidEnd (object sender, EventArgs e)
    {
        // do something
    }

    public override MvxBindingMode DefaultMode
    {
        get { return MvxBindingMode.OneWay; }
    }

    public override void SetValue(object value)
    {
        var command = value as ICommand;
        _command = command;
    }

    public override Type TargetType
    {
        get { return typeof(ICommand); }
    }

    protected override void Dispose(bool isDisposing)
    {
        if (isDisposing)
        {
            if (TextField != null)
            {
                TextField.ShouldReturn -= HandleShouldReturn;
                TextField.EditingDidBegin -= HandleEditingDidBegin;
                TextField.EditingDidEnd -= HandleEditingDidEnd;
            }
        }

        base.Dispose(isDisposing);
    }

}

My first question is: am I correct in creating one MvxPropertyInfoTargetBinding for all the events? Relatedly, I don't get the difference between MvxPropertyInfoTargetBinding and MvxTargetBinding. According to MVVMCross Binding decimal to UITextField removes decimal point the former is used when replacing an existing binding, the latter for known properties and event pairs. So am I using the correct one?

Secondly (and the real crux of my problem), my code works except for SetValue - it is fired, but the value is null. Here is what I have in my Setup file:

protected override void FillTargetFactories (IMvxTargetBindingFactoryRegistry registry)
{
    base.FillTargetFactories (registry);
    registry.RegisterPropertyInfoBindingFactory(typeof(MyTextFieldTargetBinding), typeof(UITextField), "Text");
}

I don't do anything in my View - perhaps that is where the issue lies?

EDIT:

My ViewModel:

public class LoginViewModel : MvxViewModel
{
    private string _username;
    public string Username
    { 
        get { return _username; }
        set { _username = value; RaisePropertyChanged(() => Username); }
    }

    private string _password;
    public string Password
    { 
        get { return _password; }
        set { _password = value; RaisePropertyChanged(() => Password); }
    }

    private MvxCommand _login;
    public ICommand Login
    {
        get {
            _login = _login ?? new MvxCommand(DoLogin);
            return _login;
        }
    }

    public LoginViewModel(ILoginManager loginManager)
    {
        _loginManager = loginManager;
    }

    private void DoLogin()
    {
        // call the login web service
    }
}

In my `View', I don't do anything fancy (I do create the View elements in a XIB):

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

    this.NavigationController.SetNavigationBarHidden(true, false);

    var set = this.CreateBindingSet<LoginView, Core.ViewModels.LoginViewModel>();
    set.Bind(usernameTextField).To(vm => vm.Username);
    set.Bind(passwordTextField).To(vm => vm.Password);
    set.Bind (loginButton).To (vm => vm.Login);
    set.Apply();
}

No interesting Trace messages.

回答1:

1. What is special about PropertyInfoTargetBinding?

The question you reference - MVVMCross Binding decimal to UITextField removes decimal point - gives the key to the difference between MvxTargetBinding and MvxPropertyInfoTargetBinding:

  • TargetBinding can be used for any arbitrary binding - e.g. for a non-propertyInfo-based binding
  • PropertyInfoTargetBinding inherits from TargetBinding and can only be used with actual C# Properties - because it uses PropertyInfo via Reflection.

In your case, since you aren't actually using the Text property via Reflection, then I'd be tempted not to use a PropertyInfoTargetBinding and to steer clear of the Text name as well - instead just write a custom TargetBinding.

the former is used when replacing an existing binding

This is definitely not true - instead any binding can be used to replace another binding - as the answer on the other question says

MvvmCross operates a simple 'last registered wins' system

For more on custom bindings, take a look at:

  • the N=28 video in http://mvvmcross.blogspot.co.uk/
  • take a look through some of the "standard" bindings that ship with MvvmCross
    • Droid - https://github.com/MvvmCross/MvvmCross/tree/v3.1/Cirrious/Cirrious.MvvmCross.Binding.Droid/Target
    • iOS - https://github.com/MvvmCross/MvvmCross/tree/v3.1/Cirrious/Cirrious.MvvmCross.Binding.Touch/Target
    • note that most of these use either MvxPropertyInfoTargetBinding or MvxConvertingTargetBinding as a base class

2. Why is my SetValue getting null?

Your current binding code is asking for an ICommand:

public override Type TargetType
{
    get { return typeof(ICommand); }
}

But your View code is currently binding the View to a string:

// View
set.Bind(usernameTextField).To(vm => vm.Username);

// ViewModel
private string _username;
public string Username
{ 
    get { return _username; }
    set { _username = value; RaisePropertyChanged(() => Username); }
}

To solve this...

  1. Work out what you want to bind to - is it an ICommand (e.g. and MvxCommand) or is it a string?
  2. Change the View and the Binding to reflect this.