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

.NET MAUI Image Source绑定更新后视图未刷新问题求助

问题描述

我正在开发一个采用MVVM架构和SQLite的C# .NET MAUI应用。应用包含用户ObservableCollection,点击其中项会进入对应用户的编辑视图。编辑页使用Community Toolkit的<AvatarView>展示默认png图片,点击可通过媒体选择器更换图片。

选择新图片后,模型的ImageUrl值已更新,但<AvatarView>的绑定未同步更新——新图片未显示,原图片保留,用户会误以为更换失败(实际已成功)。

新值可正确保存到SQLite数据库,点击保存按钮后,ObservableCollection也能正常更新显示新图片。我希望遵循MVVM规范,但尝试多种方法仍未解决问题。


视图代码(AddBowlerPage.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"
             xmlns:components="clr-namespace:TestApp.Views.Components"
             xmlns:vm="clr-namespace:TestApp.Views.ViewModels"
             x:DataType="vm:AddBowlerViewModel"
             x:Class="TestApp.Views.AddBowlerPage">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="70"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0"
              RowDefinitions="Auto, *"
              ColumnDefinitions="*, Auto">
            <StackLayout Spacing="5"
                         Padding="15"
                         Margin="10">
                <components:BowlerImage ImageUrl="{Binding Bowler.ImageUrl, Mode=TwoWay}">
                    <components:BowlerImage.GestureRecognizers>
                        <TapGestureRecognizer Command="{Binding SelectBowlerImageCommand}"/>
                    </components:BowlerImage.GestureRecognizers>
                </components:BowlerImage>
                <StackLayout>
                    <Button Text="Save Changes"
                            HorizontalOptions="FillAndExpand"
                            Command="{Binding SaveBowlerCommand}">
                    </Button>
                </StackLayout>
            </StackLayout>
        </Grid>
    </Grid>
</ContentPage>

代码后台(AddBowlerPage.xaml.cs)

using TestApp.Views.ViewModels;

namespace TestApp.Views;

public partial class AddBowlerPage : ContentPage
{
    public AddBowlerPage(AddBowlerViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

可复用图片组件(BowlerImage.xaml)

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Name="this"
             x:Class="TestApp.Views.Components.BowlerImage">
    <Grid BindingContext="{x:Reference this}">
        <toolkit:AvatarView ImageSource="{Binding ImageUrl}"
                            HorizontalOptions="Center"
                            VerticalOptions="Center"
                            HeightRequest="50"
                            WidthRequest="50"
                            Margin="10"
                            ZIndex="1"/>
    </Grid>
</ContentView>

视图模型(AddBowlerViewModel.cs)

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

using TestApp.Models;
using TestApp.Services;

namespace TestApp.Views.ViewModels;

[QueryProperty(nameof(Bowler), nameof(Bowler))]
public partial class AddBowlerViewModel(IDatabaseService data, IShellService shell) : BaseViewModel
{
    [ObservableProperty]
    private Bowler _bowler;

    [RelayCommand]
    private async Task SaveBowlerAsync()
    {
        if (Bowler is null)
        {
            return;
        }

        var (isValid, errorMessage) = Bowler.ValidateEmptyFields();
        if (!isValid)
        {
            await shell.DisplayAlert("Validation Error", errorMessage, "Ok");
            return;
        }

        var busyText = "Updating bowler...";
        if (Bowler.Id == 0)
        {
            busyText = "Creating bowler...";
            if (await data.IsBowlerExists(Bowler))
            {
                await shell.DisplayAlert("Validation Error", "This bowler already exists", "Ok");
                return;
            }
        }

        await ExecuteAsync(async () =>
        {
            if (!(Bowler.Id == 0
                ? await data.AddBowler(Bowler)
                : await data.UpdateBowler(Bowler)))
            {
                await shell.DisplayAlert("Update Error", "Unable to save bowler", "Ok");
            }
            await shell.ReturnToPage();
        }, busyText);
    }

    [RelayCommand]
    private async Task SelectBowlerImage()
    {
        var photo = await MediaPicker.Default.PickPhotoAsync();

        if (photo != null)
        {
            // save the file into local storage
            var localFilePath = Path.Combine(FileSystem.CacheDirectory, photo.FileName);
            using Stream sourceStream = await photo.OpenReadAsync();
            using FileStream localFileStream = File.OpenWrite(localFilePath);

            try
            {
                await sourceStream.CopyToAsync(localFileStream);
                Bowler.ImageUrl = localFilePath;
            }
            catch (ArgumentNullException ex)
            {
                await shell.DisplayAlert("Error", ex.Message, "Ok");
            }
        }
    }
}

基础视图模型(BaseViewModel.cs)

using CommunityToolkit.Mvvm.ComponentModel;

namespace TestApp.Views.ViewModels;
public partial class BaseViewModel : ObservableObject
{
    [ObservableProperty]
    private bool _isBusy;

    [ObservableProperty]
    private string _busyText;

    public async Task ExecuteAsync(Func<Task> operation, string busyText = null)
    {
        IsBusy = true;
        BusyText = busyText ?? "Processing...";
        try
        {
            await operation.Invoke();
        }
        finally
        {
            IsBusy = false;
            BusyText = "Processing...";
        }
    }
}

模型类(Bowler.cs)

using SQLite;

namespace TestApp.Models;

[Table("bowler")]
public class Bowler
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string ImageUrl { get; set; } = "account_circle.png";
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;

    public string FullName => $"{FirstName} {LastName}";
}

解决方案

问题核心是**Bowler模型未实现属性变更通知**,当ImageUrl更新时无法触发UI绑定刷新。ViewModel中的Bowler是ObservableProperty,但仅在对象本身被替换时触发通知,对象内部属性变化不会传递到UI。

具体修复步骤:

  1. 修改Bowler模型,继承ObservableObject并使用[ObservableProperty]特性自动生成属性变更通知:
using SQLite;
using CommunityToolkit.Mvvm.ComponentModel;

namespace TestApp.Models;

[Table("bowler")]
public partial class Bowler : ObservableObject
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [ObservableProperty]
    private string _imageUrl = "account_circle.png";

    [ObservableProperty]
    private string _firstName = string.Empty;

    [ObservableProperty]
    private string _lastName = string.Empty;

    public string FullName => $"{FirstName} {LastName}";
}

说明:[ObservableProperty]会自动生成公共属性(如ImageUrl)和对应的PropertyChanged通知,无需手动编写get/set逻辑。

  1. 保持现有ViewModel和XAML代码不变
    修改后,当SelectBowlerImage命令中更新Bowler.ImageUrl时,会自动触发属性变更通知,UI会立即同步显示新图片。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 03:17:05