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

.NET MAUI开发:从视图类访问ViewModel是否为不良实践?

.NET MAUI视图直接调用ViewModel方法的优化建议

我正在用.NET MAUI开发桌面应用,未采用Prism框架,仅使用内置功能实现。目前经常需要从视图类中直接调用ViewModel的方法,不确定这是否属于不良实践,也想不到其他替代方案,希望能得到优化建议。

示例代码1:删除按钮点击事件

场景:删除按钮点击后,调用ViewModel的数据库删除方法,同时操作UI布局,最后调用父ViewModel的Populate方法。

private async void DeleteButtonTapped(object sender, TappedEventArgs e)
{
    if (answer)
    {
        var bindingContext =  BindingContext as AgentDetailsPageViewModel;
        await bindingContext.DeleteFromDatabase();
        var agentsViews = this.Parent as Grid;
        agentsViews.Remove(this);
        var parentBorder = agentsViews.Parent as Border;
        parentBorder.IsVisible = false;
        var parentGrid = parentBorder.Parent as Grid;
        var agentsBorder = parentGrid.Children[0] as Border;
        Grid.SetColumnSpan(agentsBorder, 2);
        var parentBindingContext = parentGrid.BindingContext as AgentsPageViewModel;
        parentBindingContext.Populate();
    }
}

示例代码2:动态添加信用卡输入框

场景:用户可动态添加信用卡输入框,无法实现动态绑定ViewModel属性,因此从UI中遍历获取输入值,整理后传给ViewModel并调用数据库添加方法。

ObservableCollection<string> creditCardEntries = new ObservableCollection<string>();
foreach (var creditCardborderStack in creditCardbordersStack)
{
    var grid = creditCardborderStack.GetChildren<Grid>().FirstOrDefault();
    var entry = grid.GetChildren<Entry>().FirstOrDefault();
    if (entry.Text is not null)
    {
        if (entry.Text.ToCharArray()[4] != '-')
        {
            creditCardEntries.Add(Regex.Replace(entry.Text, @"(\w{4})(\w{4})(\w{4})(\w{4})", @"$1-$2-$3-$4"));
        }
        else
        {
            creditCardEntries.Add(entry.Text);
        }
    }
}
var bindingContext = (AgentAddOrEditPageViewModel)BindingContext;
if (bindingContext.AgentCreditCardNumbers is not null)
{
    bindingContext.AgentCreditCardNumbers.Clear();
}
bindingContext.AgentCreditCardNumbers = creditCardEntries;
if (await bindingContext.AddToDatabase())
{
    CloseViewAfterInsertion();
}

优化建议

1. 用ICommand替代视图事件处理

.NET MAUI内置支持Command和AsyncCommand,将按钮点击逻辑完全移到ViewModel中,避免视图直接操作ViewModel:

  • 在AgentDetailsPageViewModel中定义删除命令:
public ICommand DeleteCommand { get; }
public bool IsDeleted { get; set; } // 用于绑定控制UI可见性

public AgentDetailsPageViewModel()
{
    DeleteCommand = new AsyncCommand(ExecuteDelete);
}

private async Task ExecuteDelete()
{
    if (!answer) return;
    await DeleteFromDatabase();
    IsDeleted = true;
    // 发送消息通知父ViewModel更新
    MessagingCenter.Send(this, "AgentDeleted");
}
  • 视图中绑定命令,移除事件处理代码:
<Button Text="删除" Command="{Binding DeleteCommand}" />
<Border IsVisible="{Binding IsDeleted, Converter={StaticResource InverseBooleanConverter}}">
    <!-- 待删除的详情控件 -->
</Border>
  • 父ViewModel的布局调整逻辑,通过绑定属性实现,比如新增AgentsGridColumnSpan、DetailsPanelVisible属性,视图直接绑定这些属性即可。

2. 动态控件通过数据绑定实现

针对动态添加信用卡输入框的场景,在ViewModel中维护可观察集合,每个元素对应一个输入项的ViewModel,自动同步数据:

  • 定义信用卡项ViewModel:
public class CreditCardItemViewModel : ObservableObject
{
    private string _cardNumber;
    public string CardNumber
    {
        get => _cardNumber;
        set
        {
            // 自动格式化卡号
            var formatted = string.IsNullOrEmpty(value) ? value : Regex.Replace(value, @"(\w{4})(\w{4})(\w{4})(\w{4})", @"$1-$2-$3-$4");
            SetProperty(ref _cardNumber, formatted);
        }
    }
}
  • 主ViewModel中维护集合并提供添加命令:
public ObservableCollection<CreditCardItemViewModel> CreditCards { get; } = new();
public ICommand AddCreditCardCommand { get; }
public ICommand SaveCommand { get; }

public AgentAddOrEditPageViewModel()
{
    AddCreditCardCommand = new Command(() => CreditCards.Add(new CreditCardItemViewModel()));
    SaveCommand = new AsyncCommand(ExecuteSave);
}

private async Task ExecuteSave()
{
    var cardNumbers = CreditCards.Select(x => x.CardNumber).Where(x => !string.IsNullOrEmpty(x)).ToList();
    if (await AddToDatabase(cardNumbers))
    {
        MessagingCenter.Send(this, "CloseAfterInsert");
    }
}
  • 视图用CollectionView动态生成输入框:
<CollectionView ItemsSource="{Binding CreditCards}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Entry Text="{Binding CardNumber, Mode=TwoWay}" Placeholder="输入信用卡号" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
<Button Text="添加信用卡" Command="{Binding AddCreditCardCommand}" />
<Button Text="保存" Command="{Binding SaveCommand}" />

3. 跨ViewModel通信用消息传递

避免直接获取父ViewModel实例,用.NET MAUI内置的MessagingCenter(或CommunityToolkit.Mvvm的IMessenger)传递消息:

  • 父ViewModel注册消息监听:
public AgentsPageViewModel()
{
    MessagingCenter.Subscribe<AgentDetailsPageViewModel>(this, "AgentDeleted", (sender) =>
    {
        Populate();
        DetailsPanelVisible = false;
        AgentsGridColumnSpan = 2;
    });
}
  • 视图监听关闭消息:
public AgentAddOrEditPage()
{
    InitializeComponent();
    MessagingCenter.Subscribe<AgentAddOrEditPageViewModel>(this, "CloseAfterInsert", (sender) =>
    {
        CloseViewAfterInsertion();
    });
}

4. 明确视图与ViewModel的职责边界

  • 视图仅负责数据绑定和UI布局,不处理业务逻辑、不直接操作其他控件的状态
  • ViewModel负责所有业务逻辑、状态管理、数据处理,通过绑定属性将状态同步到视图
  • 禁止在视图中遍历UI控件获取数据,禁止直接获取其他ViewModel实例

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 22:15:32