You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Maui中原生UIView与ViewModel绑定可行性及实现方案咨询

问题:MAUI中自定义Handler渲染原生UIView后的数据绑定方案(弃用MvvmCross)

我们正在迁移一个重度依赖MvvmCross实现视图与ViewModel绑定的Xamarin.iOS应用到MAUI。目前已通过自定义Handler在ContentPage中成功渲染原生UIView,但无法实现UIView与ViewModel的绑定(比如UITextField绑定ViewModel属性、按钮绑定Command)。我们希望弃用MvvmCross,但没找到合适替代方案,想咨询是否能在MAUI中实现这类绑定,并寻求解决方法。


精简代码示例

ChangePasswordView.cs

public class ChangePasswordView : MvxViewController
{
    private GenericChangePasswordView _genericChangePasswordView;

    public UITextField _oldPasswordTextField;

    public ChangePasswordView(GenericChangePasswordView genericChangePasswordView)
    {
        _genericChangePasswordView = genericChangePasswordView;

        _oldPasswordTextField = new UITextField(new CGRect(120, 120, 300, 40))
        {
            LeftView = new UIView(new CGRect(0, 0, 10, 10)),
            LeftViewMode = UITextFieldViewMode.Always,
            BackgroundColor = UIColor.White,
            VerticalAlignment = UIControlContentVerticalAlignment.Center,
            ShouldReturn = _ =>
            {
                return false;
            },
            Placeholder = "Old password",
            SecureTextEntry = true,
        };
        Add(_oldPasswordTextField);

        var vm = _genericChangePasswordView.BindingContext as ChangePasswordViewModel;
        this.DelayBind(() =>
        {
            var set = this.CreateBindingSet<ChangePasswordView, ChangePasswordViewModel>();
            set.Bind(_oldPasswordTextField).To(m => m.OldPassword);
            set.Apply();
        });
    }
}

ChangePasswordViewModel.cs

public class ChangePasswordViewModel : MvxViewModel
{
    private decimal _oldPassword;
    public decimal OldPassword
    {
        get => _oldPassword;
        set { _oldPassword = value; RaisePropertyChanged(nameof(OldPassword)); }
    }
}

ChangePasswordViewHandler.cs(跨平台)

public partial class ChangePasswordViewHandler
{
    public static IPropertyMapper<GenericChangePasswordView, ChangePasswordViewHandler> PropertyMapper = new PropertyMapper<GenericChangePasswordView, ChangePasswordViewHandler>(ViewHandler.ViewMapper)
    {
    };

    public static CommandMapper<GenericChangePasswordView, ChangePasswordViewHandler> CommandMapper = new(ViewCommandMapper)
    {
    };

    public ChangePasswordViewHandler() : base(PropertyMapper, CommandMapper)
    {
    }
}

ChangePasswordViewHandler.iOS.cs(原生Handler)

public partial class ChangePasswordViewHandler : ViewHandler<GenericChangePasswordView, ChangePasswordView>
{
    protected override ChangePasswordView CreatePlatformView() => new ChangePasswordView(VirtualView);

    protected override void ConnectHandler(ChangePasswordView platformView)
    {
        base.ConnectHandler(platformView);
    }

    protected override void DisconnectHandler(ChangePasswordView platformView)
    {
        platformView.Dispose();
        base.DisconnectHandler(platformView);
    }
}

解决方案:基于MAUI原生绑定系统实现原生UI与ViewModel的绑定

核心思路

弃用MvvmCross后,直接利用MAUI自带的ObservableObject和数据绑定系统,通过自定义Handler中转绑定逻辑,同时在原生视图中处理UI事件与ViewModel属性的双向同步。

具体实现步骤

1. 改造ViewModel,移除MvvmCross依赖

让ViewModel继承MAUI的ObservableObject,替代MvxViewModel:

using Microsoft.Maui.Controls;

public class ChangePasswordViewModel : ObservableObject
{
    private string _oldPassword;
    public string OldPassword
    {
        get => _oldPassword;
        set => SetProperty(ref _oldPassword, value);
    }
}

注:将decimal改为string更符合密码输入场景,可根据实际需求调整类型

2. 为跨平台视图添加绑定属性

在GenericChangePasswordView中定义对应绑定属性,让MAUI绑定系统识别:

public class GenericChangePasswordView : View
{
    public static readonly BindableProperty OldPasswordProperty =
        BindableProperty.Create(nameof(OldPassword), typeof(string), typeof(GenericChangePasswordView), string.Empty, BindingMode.TwoWay);

    public string OldPassword
    {
        get => (string)GetValue(OldPasswordProperty);
        set => SetValue(OldPasswordProperty, value);
    }
}

3. 修改Handler,添加属性映射逻辑

在跨平台ChangePasswordViewHandler中,添加ViewModel到原生UI的属性同步逻辑:

public partial class ChangePasswordViewHandler
{
    public static IPropertyMapper<GenericChangePasswordView, ChangePasswordViewHandler> PropertyMapper = new PropertyMapper<GenericChangePasswordView, ChangePasswordViewHandler>(ViewHandler.ViewMapper)
    {
        [nameof(GenericChangePasswordView.OldPassword)] = MapOldPassword
    };

    public static CommandMapper<GenericChangePasswordView, ChangePasswordViewHandler> CommandMapper = new(ViewCommandMapper)
    {
        // 后续添加按钮Command映射
    };

    public ChangePasswordViewHandler() : base(PropertyMapper, CommandMapper)
    {
    }

    private static void MapOldPassword(ChangePasswordViewHandler handler, GenericChangePasswordView view)
    {
        // 同步ViewModel属性到原生UITextField
        if (handler.PlatformView._oldPasswordTextField.Text != view.OldPassword)
        {
            handler.PlatformView._oldPasswordTextField.Text = view.OldPassword;
        }
    }
}

4. 修改原生视图,处理UI事件反向更新ViewModel

移除MvvmCross相关代码,添加UITextField事件监听,同步输入内容到ViewModel:

public class ChangePasswordView : UIViewController
{
    private GenericChangePasswordView _genericChangePasswordView;
    public UITextField _oldPasswordTextField;

    public ChangePasswordView(GenericChangePasswordView genericChangePasswordView)
    {
        _genericChangePasswordView = genericChangePasswordView;

        _oldPasswordTextField = new UITextField(new CGRect(120, 120, 300, 40))
        {
            LeftView = new UIView(new CGRect(0, 0, 10, 10)),
            LeftViewMode = UITextFieldViewMode.Always,
            BackgroundColor = UIColor.White,
            VerticalAlignment = UIControlContentVerticalAlignment.Center,
            ShouldReturn = _ =>
            {
                _oldPasswordTextField.ResignFirstResponder();
                return true;
            },
            Placeholder = "Old password",
            SecureTextEntry = true,
        };
        // 添加文本变化事件,同步到ViewModel
        _oldPasswordTextField.EditingChanged += OnOldPasswordChanged;
        Add(_oldPasswordTextField);
    }

    private void OnOldPasswordChanged(object sender, EventArgs e)
    {
        if (_genericChangePasswordView.BindingContext is ChangePasswordViewModel vm)
        {
            vm.OldPassword = _oldPasswordTextField.Text;
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _oldPasswordTextField.EditingChanged -= OnOldPasswordChanged;
        }
        base.Dispose(disposing);
    }
}

5. 在ContentPage中配置绑定

在XAML中完成跨平台视图与ViewModel的绑定:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:YourNamespace"
             x:Class="YourNamespace.ChangePasswordPage">
    <ContentPage.BindingContext>
        <local:ChangePasswordViewModel />
    </ContentPage.BindingContext>
    <local:GenericChangePasswordView OldPassword="{Binding OldPassword}" />
</ContentPage>

按钮Command绑定扩展示例

如果需要绑定按钮Command,可按以下步骤添加:

  1. 在GenericChangePasswordView中添加Command绑定属性:
public static readonly BindableProperty SubmitCommandProperty =
    BindableProperty.Create(nameof(SubmitCommand), typeof(ICommand), typeof(GenericChangePasswordView));

public ICommand SubmitCommand
{
    get => (ICommand)GetValue(SubmitCommandProperty);
    set => SetValue(SubmitCommandProperty, value);
}
  1. 在Handler的CommandMapper中添加映射:
[nameof(GenericChangePasswordView.SubmitCommand)] = MapSubmitCommand
  1. 实现映射方法并绑定按钮点击事件:
private static void MapSubmitCommand(ChangePasswordViewHandler handler, GenericChangePasswordView view)
{
    var button = handler.PlatformView._submitButton;
    button.TouchUpInside -= OnSubmitButtonTapped;
    if (view.SubmitCommand != null)
    {
        button.TouchUpInside += OnSubmitButtonTapped;
    }
}

private static void OnSubmitButtonTapped(object sender, EventArgs e)
{
    var handler = (ChangePasswordViewHandler)sender;
    handler.VirtualView.SubmitCommand?.Execute(null);
}

内容的提问来源于stack exchange,提问作者caz162

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:02:34