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

Xamarin C# 如何在ViewModel编写代码并绑定XAML设计页面

.NET MAUI/Xamarin.Forms 逻辑迁移到ViewModel绑定实现方案

你现有代码存在两个核心问题会导致绑定失效:

  1. BindableLayout未声明ItemTemplate节点包裹单条项布局,无法正确遍历集合渲染列表
  2. 列表项内部的绑定上下文为当前遍历到的单个Coupon实体对象,直接绑定命令无法找到页面级ViewModel中定义的命令属性

实现步骤

1. 定义列表项实体类

优惠券实体需要实现属性变更通知接口,保证属性修改时UI能同步更新:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class Coupon : INotifyPropertyChanged
{
    private string _couponName;
    public string CouponName
    {
        get => _couponName;
        set { _couponName = value; OnPropertyChanged(); }
    }

    private string _imageUrl;
    public string ImageUrl
    {
        get => _imageUrl;
        set { _imageUrl = value; OnPropertyChanged(); }
    }

    private string _couponLimit;
    public string CouponLimit
    {
        get => _couponLimit;
        set { _couponLimit = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2. 编写页面ViewModel

所有业务逻辑、数据加载逻辑、命令逻辑全部放在ViewModel中,注意列表集合必须使用ObservableCollection<T>才能自动触发UI刷新:

using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.Maui.Controls; // Xamarin.Forms 引用对应命名空间即可

public class CouponPageViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Coupon> _coupons = new ObservableCollection<Coupon>();
    /// <summary>
    /// 绑定到BindableLayout.ItemsSource的优惠券集合
    /// </summary>
    public ObservableCollection<Coupon> Coupons
    {
        get => _coupons;
        set { _coupons = value; OnPropertyChanged(); }
    }

    /// <summary>
    /// 优惠券点击命令
    /// </summary>
    public ICommand CouponTappedCommand { get; }

    public CouponPageViewModel()
    {
        // 初始化命令,指定参数类型为Coupon,接收当前点击的项
        CouponTappedCommand = new Command<Coupon>(OnCouponTapped);
        // 加载初始数据,替换为你原代码后置中的数据加载逻辑
        LoadCouponData();
    }

    private void LoadCouponData()
    {
        // 示例数据,替换为实际接口/本地数据读取逻辑
        Coupons.Add(new Coupon
        {
            CouponName = "满200减50",
            ImageUrl = "coupon_50.png",
            CouponLimit = "有效期至2024-12-31"
        });
        Coupons.Add(new Coupon
        {
            CouponName = "满100减20",
            ImageUrl = "coupon_20.png",
            CouponLimit = "有效期至2024-12-31"
        });
    }

    /// <summary>
    /// 原代码后置中的优惠券点击逻辑,迁移到此处
    /// </summary>
    private void OnCouponTapped(Coupon tappedCoupon)
    {
        if (tappedCoupon == null) return;
        // 编写实际点击逻辑:领券、跳转详情、弹提示等
        Application.Current.MainPage.DisplayAlert("操作提示", $"已选中优惠券:{tappedCoupon.CouponName}", "确定");
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

3. 修正XAML绑定代码

补全缺失的ItemTemplate节点,修正命令绑定路径,通过相对源绑定向上查找页面级ViewModel的命令,同时传递当前点击项作为命令参数:

<StackLayout BindableLayout.ItemsSource="{Binding Coupons}"
             Spacing="0"
             HorizontalOptions="FillAndExpand"
             Margin="5,5,5,0">
    <!-- 必须声明ItemTemplate包裹单条项布局 -->
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <Frame HorizontalOptions="FillAndExpand"
                   Margin="20,10"
                   Padding="20,10"
                   BorderColor="Maroon"
                   BackgroundColor="White"
                   CornerRadius="10"
                   HasShadow="True">
                <StackLayout Orientation="Horizontal">
                    <StackLayout>
                        <Label Text="{Binding CouponName}"
                               FontSize="Large"
                               HorizontalOptions="Center"
                               FontAttributes="Bold"/>
                        <BoxView HeightRequest="1"
                                 HorizontalOptions="FillAndExpand"/>
                        <Image Source="{Binding ImageUrl}"
                               HorizontalOptions="FillAndExpand"
                               VerticalOptions="StartAndExpand"/>
                        <Label Text="{Binding CouponLimit}"
                               Margin="0,10,0,0"
                               FontSize="Large"
                               HorizontalOptions="Center" />
                    </StackLayout>
                </StackLayout>
                <Frame.GestureRecognizers>
                    <TapGestureRecognizer NumberOfTapsRequired="1"
                                          <!-- 相对源绑定找到页面级ViewModel的命令 -->
                                          Command="{Binding BindingContext.CouponTappedCommand, Source={RelativeSource AncestorType={x:Type Page}}}"
                                          <!-- 将当前点击的Coupon实体作为参数传入命令 -->
                                          CommandParameter="{Binding .}" />
                </Frame.GestureRecognizers>
            </Frame>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</StackLayout>

4. 配置页面绑定上下文

代码后置文件中仅保留绑定上下文赋值逻辑,不再编写业务代码:

public partial class CouponPage : ContentPage
{
    public CouponPage()
    {
        InitializeComponent();
        // 指定页面绑定上下文为对应ViewModel实例
        BindingContext = new CouponPageViewModel();
    }
}

优化建议

  • 如果项目引入了CommunityToolkit.Mvvm库,可以通过源生成器大幅简化ViewModel代码,无需手动编写属性变更通知和命令的重复模板代码
  • 不要在ViewModel中直接操作UI控件,所有UI交互逻辑通过命令、属性绑定实现,页面跳转、弹窗等逻辑可以通过注册导航服务的方式解耦,避免ViewModel和页面强耦合

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:18:24