如何创建适配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
相关产品推荐
相关产品推荐

