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

WinUI 3 ImageViewer用户控件ViewModel设计与数据绑定疑问

WinUI 3 ImageViewer UserControl 实现疑问

我需要在WinUI 3中实现一个名为ImageViewer的UserControl,具备照片显示、遍历、添加和删除功能。该控件需要包含以下属性:

  • ICollection<Product_PhotoModel> Photos:用于访问图片模型集合
  • DependencyProperty PhotosProperty:创建可在XAML中绑定的属性(XAML属性名称无需与集合属性名一致)
  • int CurrentPhotoIndex:跟踪当前显示照片的索引,避免无效索引访问
  • string CurrentPhotoDescription:跟踪当前照片的描述
  • ImageViewerViewModel ViewModel:用于存放数据与业务逻辑

由于ImageViewer继承自UserControl,点击上一张/下一张/添加/删除按钮时,Photos集合及Current*系列属性需要能通知UI更新(例如删除当前照片后自动显示最后一张)。我了解到需要继承ObservableRecipient并使用OnPropertyChanged方法实现数据通知,因此考虑将Photos集合放在ViewModel而非代码后置文件(ImageViewer.xaml.cs)中,来实现照片的遍历、增删操作。但参考的示例并未使用ViewModel,我不清楚如何将DependencyProperty的数据直接与ViewModel关联。

我的疑问如下:

  1. 在UserControl中使用ViewModel是否是合理的做法?
  2. 如果使用ViewModel是合适的最佳实践,是否需要在代码后置文件和ViewModel中分别维护模型集合并同步,还是仅在ViewModel中维护?
  3. 如果使用ViewModel并非好方案(实现困难,存在更简便的方式),如何让Photos集合具备Observable特性?(注:我已将其初始化为ObservableCollection<Product_PhotoModel>,但无法正常显示图片)

相关代码

ImageViewer.xaml

<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->

<UserControl
    x:Class="MyApp.Desktop.UserControls.ImageViewer"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyApp.Desktop.UserControls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:dxe="using:DevExpress.WinUI.Editors"
    mc:Ignorable="d">

    <Grid Padding="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal"
                    Grid.Row="0">
            <Grid>
                <Image Source="{x:Bind CurrentPhotoPath, Mode=TwoWay}"
                       Stretch="Uniform"/>

                <Button Click="BtnPrevPhoto_Click"
                VerticalAlignment="Center" HorizontalAlignment="Left"
                Style="{StaticResource PreviousButtonStyle}"/>

                <Button Click="BtnNextPhoto_Click"
                VerticalAlignment="Center" HorizontalAlignment="Right"
                Style="{StaticResource NextButtonStyle}"/>
            </Grid>

            <StackPanel Orientation="Vertical"
                        VerticalAlignment="Center"
                        Margin="{StaticResource SmallLeftMargin}"
                        Grid.Column="1">
                <Button Click="BtnAddPhoto_Click"
                        Margin="{StaticResource XXSmallBottomMargin}">
                    <FontIcon Glyph="&#xe109;" Style="{StaticResource SmallFontIconStyle}"/>
                </Button>
                
                <Button Click="BtnDeletePhoto_Click"
                        Margin="{StaticResource XXSmallTopMargin}">
                    <FontIcon Glyph="&#xe107;" Style="{StaticResource SmallFontIconStyle}"/>
                </Button>
            </StackPanel>
        </StackPanel>

        <dxe:TextEdit x:Uid="/Products/TxtPhotoDescription"
                      Text="{x:Bind CurrentPhotoDescription, Mode=TwoWay}"
                      IsReadOnly="{x:Bind IsReadOnly, Mode=OneWay}"
                      Style="{StaticResource FormSmallTextEditStyle}"
                      Margin="{StaticResource SmallTopMargin}"
                      HorizontalAlignment="Stretch"
                      Grid.Row="1"/>
        
    </Grid>
</UserControl>

ImageViewer.xaml.cs

// Copyright (c) Microsoft Corporation and Contributors.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using DevExpress.Mvvm.Native;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using MyApp.Desktop.Helpers;
using MyApp.Desktop.ViewModels.UserControls;
using MyApp.Models.FileStorage;
using MyApp.Models.Products;

namespace ThemelioApp.Desktop.UserControls;

public sealed partial class ImageViewer : UserControl
{
    public static readonly DependencyProperty PhotosProperty = DependencyProperty.Register(
    "Photos", typeof(ICollection<Product_PhotoModel>), typeof(ImageViewer), new PropertyMetadata(null));

    public ICollection<Product_PhotoModel> Photos
    {
        get => (ICollection<Product_PhotoModel>)GetValue(PhotosProperty);
        set => SetValue(PhotosProperty, value);
    }

    public static readonly DependencyProperty PhotosToDeleteProperty = DependencyProperty.Register(
"PhotosToDelete", typeof(ICollection<long>), typeof(ImageViewer), new PropertyMetadata(null));

    public ICollection<long> PhotosToDelete
    {
        get => (ICollection<long>)GetValue(PhotosToDeleteProperty);
        set => SetValue(PhotosToDeleteProperty, value);
    }

    public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register(
    "IsReadOnlyProperty", typeof(bool), typeof(ImageViewer), new PropertyMetadata(null));

    public bool IsReadOnly
    {
        get => (bool)GetValue(IsReadOnlyProperty);
        set => SetValue(IsReadOnlyProperty, value);
    }

    public int CurrentPhotoIndex
    {
        get; set;
    }

    private readonly string _noPhotoPath;

    public string CurrentPhotoPath
    {
        get; set;
    }

    public string CurrentPhotoDescription
    {
        get; set;
    }

    public ImageViewerViewModel ViewModel
    {
        get;
    }

    public ImageViewer()
    {
        InitializeComponent();

        ViewModel = App.GetService<ImageViewerViewModel>();

        Photos = new ObservableCollection<Product_PhotoModel>();

        PhotosToDelete = new ObservableCollection<long>();

        CurrentPhotoIndex = 0;

        _noPhotoPath = new Uri("ms-appx:///assets/Images/NoImage128x128.png").LocalPath;

        CurrentPhotoPath = Photos.Count == 0 ? _noPhotoPath : Photos.FirstOrDefault().Photo.LocalFilePath;

        CurrentPhotoDescription = "";
    }

    private async void BtnAddPhoto_Click(object sender, RoutedEventArgs e)
    {
        var file = await FileStorageHelper.PickFile(FileStorageHelper.PickFileType.image, true);
        if (file == null)
        {
            return;
        }

        AddPhotoLocal(file.Path);
    }

    private async void BtnDeletePhoto_Click(object sender, RoutedEventArgs e)
    {
        RemovePhotoLocal();
    }

    private void BtnNextPhoto_Click(object sender, RoutedEventArgs e)
    {
        Next();
    }

    private void BtnPrevPhoto_Click(object sender, RoutedEventArgs e)
    {
        Previous();
    }

    public void Next()
    {
        CurrentPhotoIndex++;
        UpdateCurrentPhoto();
    }

    public void Previous()
    {
        CurrentPhotoIndex--;
        UpdateCurrentPhoto();
    }

    public void AddPhotoLocal(string path)
    {
        Photos.Add(
            new Product_PhotoModel()
            {
                Photo = new FileStorageDetailedModel()
                {
                    Description = "",
                    LocalFilePath = path
                }
            });
    }

    public void RemovePhotoLocal()
    {
        PhotosToDelete ??= new List<long>();

        var toDelete = Photos.ElementAt(CurrentPhotoIndex);
        if (toDelete.Id > 0)
        {
            PhotosToDelete.Add(toDelete.Photo.Id);
        }

        Photos.Remove(toDelete);

        UpdateCurrentPhoto();
    }

    public void UpdateCurrentPhoto()
    {
        if (CurrentPhotoIndex > Photos?.Count - 1)
        {
            CurrentPhotoIndex = (int)(Photos?.Count - 1);
        }
        else if (CurrentPhotoIndex < 0)
        {
            CurrentPhotoIndex = 0;
        }

        CurrentPhotoPath = Photos.ElementAt(CurrentPhotoIndex).Photo.LocalFilePath;
        CurrentPhotoDescription = Photos.ElementAt(CurrentPhotoIndex).Photo.Description;
    }
}

ImageViewerViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using MyApp.Desktop.UserControls;
using MyApp.Models.FileStorage;
using MyApp.Models.Products;

namespace MyApp.Desktop.ViewModels.UserControls;

public class ImageViewerViewModel : ObservableRecipient
{

    public ImageViewerViewModel()
    {
    }

}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 03:17:02