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

如何创建适配MVVM、样式同默认的.NET MAUI自定义弹窗?

.NET MAUI 自定义MVVM弹窗(含多控件+匹配默认样式)

需求说明

需要创建包含Label、DatePicker、TimePicker、TextBox、FilePicker等多控件的自定义弹窗,要求适配MVVM模式,且样式与.NET MAUI默认弹窗一致。目前已尝试用模态ContentPage实现,但无法匹配默认弹窗样式,寻求最优方案。

现有尝试代码

NewAppointment.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="App.Controls.Popups.NewAppointmentPage"
             Title="NewAppointmentPage">
  <ContentPage.Content>
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
      <Frame VerticalOptions="Center" CornerRadius="20" BackgroundColor="White">
        <StackLayout Padding="50,50,50,50">
          <ContentView x:Name="ContentView"/>
          <StackLayout Orientation="Horizontal">
            <Button Text="Confirm"></Button>
            <Button Text="Cancel"></Button>
          </StackLayout>
        </StackLayout>
      </Frame>
    </StackLayout>
  </ContentPage.Content>
</ContentPage>

NewAppointment.xaml.cs

using System.Windows.Command;

namespace App.Controls.Popups;

public partial class NewAppointmentPage : ContentPage
{
  public NewAppointmentPage()
  {
    InitializeComponent();
    this.BackgroundColor = Color.FromArgb("#80000000");
  }

  public static readonly BindableProperty PopupContentProperty = BindableProperty.Create(
      propertyName: nameof(PopupContent),
      returnType: typeof(View),
      declaringType: typeof(NewAppointmentPage),
      defaultValue: null,
      defaultBindingMode: BindingMode.OneWay,
      propertyChanged: PopupContentPropertyChanged);

  private static void PopupContentPropertyChanged(BindableObject bindable, object oldValue, object newValue)
  {
    NewAppointmentPage controls = (NewAppointmentPage)bindable;
    if (newValue != null)
      controls.ContentView.Content = (View)newValue;
  }

  private bool IsVisible;

  public View PopupContent
  {
    get => (View)GetValue(PopupContentProperty);
    set { SetValue(PopupContentProperty, value); }
  }

  public ICommand PopModelCommand => new Command(async () =>
  {
    await App.Current.MainPage.Navigation.PopModalAsync();
  });
}

HomeViewModel.cs

private async void AddNewRecord()
{
  await App.Current.MainPage.Navigation.PushModalAsync(new NewAppointmentPage());
}

最优解决方案:使用.NET MAUI Popup控件

官方Popup控件是专门为弹窗场景设计的,默认自带半透明背景、居中布局,样式更贴合系统默认弹窗,且更轻量(无需操作导航栈)。以下是适配MVVM的完整实现:

1. 创建自定义弹窗(AppointmentPopup.xaml)

<?xml version="1.0" encoding="utf-8" ?>
<Popup xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       x:Class="App.Controls.Popups.AppointmentPopup"
       Size="350, 500"
       BackgroundColor="#80000000">
    <VerticalStackLayout Padding="20" Spacing="15" BackgroundColor="White" CornerRadius="12">
        <!-- 标题 -->
        <Label Text="新建预约" FontSize="Title" FontAttributes="Bold" HorizontalOptions="Center"/>
        
        <!-- 日期选择 -->
        <Label Text="预约日期"/>
        <DatePicker Date="{Binding AppointmentDate}" />
        
        <!-- 时间选择 -->
        <Label Text="预约时间"/>
        <TimePicker Time="{Binding AppointmentTime}" />
        
        <!-- 备注输入 -->
        <Label Text="备注"/>
        <Editor Placeholder="输入备注..." Text="{Binding Remarks}" HeightRequest="80"/>
        
        <!-- 文件选择按钮 -->
        <Button Text="选择附件" Command="{Binding PickFileCommand}"/>
        <Label Text="{Binding SelectedFileName}" FontSize="Small" TextColor="Gray"/>
        
        <!-- 操作按钮 -->
        <HorizontalStackLayout Spacing="10" HorizontalOptions="End">
            <Button Text="取消" Command="{Binding CancelCommand}" BackgroundColor="LightGray" TextColor="Black"/>
            <Button Text="确认" Command="{Binding ConfirmCommand}" BackgroundColor="{StaticResource Primary}" TextColor="White"/>
        </HorizontalStackLayout>
    </VerticalStackLayout>
</Popup>

2. 弹窗后台代码(AppointmentPopup.xaml.cs)

namespace App.Controls.Popups;

public partial class AppointmentPopup : Popup
{
    public AppointmentPopup(object viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

3. 弹窗ViewModel(AppointmentPopupViewModel.cs)

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Maui.Storage;

namespace App.ViewModels;

public partial class AppointmentPopupViewModel : ObservableObject
{
    // 绑定的弹窗数据
    [ObservableProperty]
    private DateTime appointmentDate = DateTime.Now;
    
    [ObservableProperty]
    private TimeSpan appointmentTime = DateTime.Now.TimeOfDay;
    
    [ObservableProperty]
    private string remarks = string.Empty;
    
    [ObservableProperty]
    private string selectedFileName = string.Empty;

    // 弹窗回调(用于传递结果给主ViewModel)
    private readonly Action<AppointmentInfo>? _onConfirm;

    public AppointmentPopupViewModel(Action<AppointmentInfo>? onConfirm = null)
    {
        _onConfirm = onConfirm;
    }

    // 确认命令
    [RelayCommand]
    private async Task Confirm()
    {
        var info = new AppointmentInfo
        {
            Date = AppointmentDate.Date + AppointmentTime,
            Remarks = Remarks,
            SelectedFile = SelectedFileName
        };
        
        _onConfirm?.Invoke(info);
        await Shell.Current.CurrentPage.ClosePopupAsync();
    }

    // 取消命令
    [RelayCommand]
    private async Task Cancel()
    {
        await Shell.Current.CurrentPage.ClosePopupAsync();
    }

    // 文件选择命令
    [RelayCommand]
    private async Task PickFile()
    {
        var result = await FilePicker.Default.PickAsync(new PickOptions
        {
            PickerTitle = "选择附件"
        });
        
        if (result != null)
        {
            SelectedFileName = result.FileName;
        }
    }
}

// 用于传递弹窗结果的实体类
public class AppointmentInfo
{
    public DateTime Date { get; set; }
    public string Remarks { get; set; } = string.Empty;
    public string SelectedFile { get; set; } = string.Empty;
}

4. 主ViewModel调用弹窗(HomeViewModel.cs)

using CommunityToolkit.Mvvm.Input;
using App.Controls.Popups;

namespace App.ViewModels;

public partial class HomeViewModel : ObservableObject
{
    [RelayCommand]
    private async Task AddNewRecord()
    {
        var popupViewModel = new AppointmentPopupViewModel(OnAppointmentConfirmed);
        var popup = new AppointmentPopup(popupViewModel);
        
        await Shell.Current.CurrentPage.ShowPopupAsync(popup);
    }

    // 处理弹窗确认后的逻辑
    private void OnAppointmentConfirmed(AppointmentInfo info)
    {
        // 这里处理获取到的预约数据,比如保存到数据库
        Console.WriteLine($"预约日期:{info.Date},备注:{info.Remarks},附件:{info.SelectedFile}");
    }
}

关键说明

  • 样式匹配:通过设置Popup.BackgroundColor="#80000000"实现半透明遮罩,内容区使用White背景和CornerRadius="12",和默认弹窗的圆角、背景风格一致;控件使用系统默认样式,确保跨平台适配。
  • MVVM适配:使用CommunityToolkit.Mvvm的ObservableObject和RelayCommand简化绑定逻辑,弹窗通过构造函数传入ViewModel,避免代码耦合。
  • 结果传递:通过回调函数Action<AppointmentInfo>将弹窗数据传递给主ViewModel,符合MVVM的单向数据流原则。
  • 优势:相比模态ContentPage,Popup无需操作导航栈,性能更优,且默认行为更符合弹窗的交互预期(点击遮罩可关闭等,可通过CanBeDismissedByTappingOutside属性控制)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 06:55:18