I want to create a UserControl in WinUI 3 called ImageViewer that will show, traverse, add and delete photos.
My UserControl contains the following properties:
Since ImageViewer is a UserControl it inherits from class UserControl. For the Photos to be able to change when pressing the Previous/Next/Add/Delete buttons the Photos Collection and Current* Properties have to be able to notify the UI that something has changed (i.e. the current photo was deleted, thus show the last one). However, my understanding is that a class must inherit from ObservableRecipient and thus use the OnPropertyChanged method on its data (e.g. Photos, CurrentPhotoIndex). My thinking is that the Photos collection must not be in the code-behind (ImageViewer.xaml.cs) but in the ViewModel (ImageViewerViewModel) and then I will be able to traverse/add/delete photos.
My problem is that in the example (from which I took inspiration from) did not use a ViewModel and I don't know how to set the DependencyProperty data to be directly from the ViewModel.
My questions:
I hope that my question is well posed. And if anyone needs some more information, I will be more than happy to provide it.
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="" Style="{StaticResource SmallFontIconStyle}"/>
</Button>
<Button Click="BtnDeletePhoto_Click"
Margin="{StaticResource XXSmallTopMargin}">
<FontIcon Glyph="" 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 (currently empty as I couldn't get it to work)
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()
{
}
}
Inside your UserControl
, you can just x:Name
your Image
control so you can change what image to show from code-behind.
And, in my opinion, a UserControl
should be just its *.xaml and *.xaml.cs. And should be reusable and available for ViewModels.
You also need to consider what is the responsibility of your ImageViewer control.
For example:
Just an image viewer
Expose CurrentImage, PreviousCommand, NextCommand, AddCommand, RemoveCommand, etc... as DependencyProperties
and let a ViewModel do all the work.
Just image navigation
Expose ItemsSource and do navigation in code-behind, and expose AddCommand, RemoveCommand and let a ViewModel do add/remove from the ItemsSource collection in the ViewModel.
Image navigation and add/remove
Expose Directory as DependencyProperty
and do everthing (including image loading) in code-behind.