Возможно ли сделать конвертер для изображений .NET MAUI

Перед уточнением вопроса вот немного вводных данных:

  1. В workbench MYSQL есть бд с таблицей Installation.
  2. В этой таблице есть столбец Image, в котором будет хранится полный путь к изображению либо его название имеющее тип данных string.
  3. Я не использую MVVM подход и хотелось бы решение не в таком подходе.
  4. В мобильном приложении есть статьи, в которых отображаются данные из этой таблицы. Отображаются они так, что изображение слева, а данные справа.

Чтобы добавлять статьи у меня есть отдельная страница в приложении где я ввожу данные и буду вводить полный путь изображение или его url ссылку, который будет храниться в бд,так вот вопрос, как реализовать конвертер, который бы брал текстовые данные из поля Image находящегося в таблице Installation и конвертировал бы эти данные в изображение.Перерыл весь интернет и не нашел способа его реализовать, там в основном все делали через запросы к серверу,но у меня нету отдельного сервера в котором я мог бы его хранить,я использую обычный localhost. Не судите строго,я не так давно изучаю maui. Вот как выглядит структура таблицы Installation

public class Installation
  {
      public int Id { get; set; }
      public string Title { get; set; }
      public string Description { get; set; }
      public string NameOs { get; set; }
      public string WhereInstall { get; set; }
      public string ImageUri {get;set; }
      public DateTime Create_dt { get; set; }
      public DateTime? Update_dt { get; set; }

      public int TypeId { get; set; } = 4;
      public TypeStatia TypeStatias { get; set; }
      public bool ShowAdminButtons { get; set; } = true;

      public Installation() { }

      public Installation(
          string title,
          string description,
          string nameOs,
          string whereInstall)
      {
          Title = title;
          Description = description;
          NameOs = nameOs;
          WhereInstall = whereInstall;
          Create_dt = DateTime.Now;
          Update_dt = DateTime.Now;
      }
  }

код страницы, которая отображает статьи с данными из таблицы Installation:

using IbragimovLinux.DatabaseContext;
using IbragimovLinux.Entities;
using IbragimovLinux.ValidationPages;

namespace IbragimovLinux.Windows;

public partial class InstallationWindow : ContentPage
{
    public InstallationWindow()
    {
        InitializeComponent();
    }

       protected override void OnAppearing()
    {
        RefreshCollectionView();
        LoadData();


        var roleId = ApplicationData.CurrentUser!.Id;
        CheckRolePermissions();

    }

    private void CheckRolePermissions()
    {
        bool isAdmin = ApplicationData.CurrentUser?.RoleId == 1;

        AddButton.IsVisible = isAdmin;
    }

    private void LoadData()
    {
        using (var db = new ApplicationDbContext())
        {
            var install = db.Installations.ToList();
            if (ApplicationData.CurrentUser?.RoleId != 1)
            {
                foreach (var item in install)
                {
                    item.ShowAdminButtons = false;
                }
            }
            InstallationLV.ItemsSource = install;
        }
    }

    private void AddButton_Clicked(object sender, EventArgs e)
    {
  
        AppShell.Current.GoToAsync(nameof(AddInfo), true);
    }

    private async void Delete_Clicked(object sender, EventArgs e)
    {
        if (ApplicationData.CurrentUser?.RoleId != 1)
        {
            await DisplayAlert("Ошибка", "Недостаточно прав для удаления", "OK");
            return;
        }

        // Получаем статью для удаления
        var button = (ImageButton)sender;
        var article = (Installation)button.BindingContext;

        // Запрашиваем подтверждение
        bool confirm = await DisplayAlert(
            "Удаление статьи",
            $"Вы уверены, что хотите удалить статью \"{article.Title}\"?",
            "Да", "Нет");

        if (confirm)
        {
            try
            {
                // Удаляем из базы данных
                using (var db = new ApplicationDbContext())
                {
                    db.Installations.Remove(article);
                    await db.SaveChangesAsync();
                    LoadData();
                }

                await DisplayAlert("Успех", "Статья удалена", "OK");
            }
            catch (Exception ex)
            {
                await DisplayAlert("Ошибка", $"Не удалось удалить статью: {ex.Message}", "OK");
            }
        }
    }

    private void EditButton_Clicked(object sender, EventArgs e)
    {
        AppShell.Current.GoToAsync(nameof(EditPage), true);
    }

    private void RefreshData(object sender, EventArgs e)
    {
        RefreshCollectionView();
        RefreshLV.IsRefreshing = false;
    }

    private void RefreshCollectionView()
    {
        ApplicationDbContext dbContext = new ApplicationDbContext();
        InstallationLV.ItemsSource = dbContext.Installations.ToList();
    }

    private async void Tap_event(object sender, TappedEventArgs e)
    {
        if (sender is Border border && border.BindingContext is Installation installation)
        {
           
           DataTransfer.CurrentId = installation.Id;
           DataTransfer.CurrentType = installation.TypeId;

            await Navigation.PushAsync(new DetailPage());
        }
    }
}

Xaml часть этой страницы:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="IbragimovLinux.Windows.InstallationWindow"
             xmlns:entities="clr-namespace:IbragimovLinux.Entities"
             Title="Установка"
             BackgroundColor="LightGrey">


    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <!-- Кнопки "Выйти" и "Добавить" -->
            <RowDefinition Height="Auto"/>
            <!-- Заголовок таблицы -->
            <RowDefinition Height="*"/>
            <!-- Список данных -->
        </Grid.RowDefinitions>

        <!--<HorizontalStackLayout Grid.Row="0" Spacing="10" Padding="10">
            <SearchBar 
                x:Name="SearchBar" 
                Placeholder="Search articles..." 
                WidthRequest="250"
                HorizontalOptions="StartAndExpand"/>-->
        
        <HorizontalStackLayout 
            Grid.Row="1"
            Padding="10"
            HorizontalOptions="End">

            <ImageButton 
             x:Name="AddButton" 
             Source="addbutton.png" 
             WidthRequest="30" 
             HeightRequest="30" 
             Clicked="AddButton_Clicked" 
             IsVisible="{Binding ShowAdminButtons}"/>
        </HorizontalStackLayout>

        <!-- RefreshView для обновления списка -->
        <RefreshView 
         Grid.Row="2" 
         x:Name="RefreshLV" 
         Refreshing="RefreshData">

            <ScrollView>
                <CollectionView x:Name="InstallationLV">
                    <CollectionView.ItemTemplate>
                        <DataTemplate x:DataType="{x:Type entities:Installation}">

                            <Border 
                             StrokeThickness="0.5"
                             Stroke="Black"
                             Padding="10,15">

                                    <Grid>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="Auto"/>
                                            <ColumnDefinition Width="*"/>
                                            <ColumnDefinition Width="Auto"/>
                                        </Grid.ColumnDefinitions>
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="Auto"/>
                                    </Grid.RowDefinitions>

                                    <Image Grid.RowSpan="3"
                                           Source="D:\Ibra\IbragimovLinux\IbragimovLinux\Resources\Images\dotnetbot.png"
                                           WidthRequest="60"
                                           HeightRequest="60"
                                           Aspect="AspectFill"/>

                                    <Label 
                                         Grid.Column="1" 
                                         Grid.Row="0" 
                                         Text="{Binding Title}" 
                                         MaxLines="1" 
                                         LineBreakMode="TailTruncation"
                                         FontSize="18"
                                         FontAttributes="Bold"
                                         TextColor="Black"/>

                                    <Label 
                                         Grid.Column="1" Grid.Row="1"
                                         FontSize="14"
                                         TextColor="Blue"
                                         Text="{Binding Create_dt}"/>

                                    <Label 
                                        Grid.Column="1"
                                        Grid.Row="2"
                                        FontSize="16"
                                        TextColor="#333333"
                                        Text="{Binding Description, StringFormat='Описание: {0}'}" 
                                        LineBreakMode="TailTruncation" 
                                         MaxLines="3"/>

                                    <HorizontalStackLayout Grid.Column="2" Grid.RowSpan="3"
                                                           VerticalOptions="Center"
                                                           Spacing="10"
                                                           IsVisible="{Binding ShowAdminButtons}">
                                        <ImageButton
                                         Source="delete.png" 
                                         WidthRequest="25" 
                                         HeightRequest="20" 
                                         Clicked="Delete_Clicked" />


                                        <ImageButton 
                                         Source="write.png" 
                                         WidthRequest="25" 
                                         HeightRequest="20" 
                                         Clicked="EditButton_Clicked"/>
                                    </HorizontalStackLayout>
                                </Grid>

                                <Border.GestureRecognizers>
                                    <TapGestureRecognizer Tapped="Tap_event" CommandParameter="{Binding}"/>
                                </Border.GestureRecognizers>

                            </Border>

                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
            </ScrollView>

        </RefreshView>
    </Grid>
</ContentPage>

Код страницы добавления:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="IbragimovLinux.ValidationPages.AddInfo"
             Title="Добавление статьи">
    <ScrollView>
        <StackLayout Padding="20" Spacing="10">
            <!-- ComboBox для выбора таблицы -->
            <Picker x:Name="TablePicker" Title="Выберите таблицу" HorizontalOptions="FillAndExpand">
                <Picker.Items>
                    <x:String>Codes</x:String>
                    <x:String>Errors</x:String>
                    <x:String>Installations</x:String>
                    <x:String>Fixes</x:String>
                    <x:String>Updates</x:String>
                </Picker.Items>
            </Picker>

            <!-- Динамические поля для ввода данных -->
            <StackLayout x:Name="InputFieldsLayout" Spacing="10"/>

            <!-- Кнопка для добавления данных -->
            <Button Text="Добавить данные" Clicked="AddDataButton_Clicked" Grid.Row="2"/>
        </StackLayout>
    </ScrollView>
</ContentPage>

Xaml.cs часть страницы:

using System.Xml.Linq;
using IbragimovLinux.DatabaseContext;
using Microsoft.EntityFrameworkCore;
using IbragimovLinux.Entities;
using System;

namespace IbragimovLinux.ValidationPages;

public partial class AddInfo : ContentPage
{
    private ApplicationDbContext _db = new();
    private Dictionary<string, Entry> _inputFields = new Dictionary<string, Entry>(); // Поля для ввода данных
    public AddInfo()
    {
        InitializeComponent();

        TablePicker.SelectedIndexChanged += TablePicker_SelectedIndexChanged;
    }

    private void TablePicker_SelectedIndexChanged(object sender, EventArgs e)
    {
        var selectedTable = TablePicker.SelectedItem as string;
        if (string.IsNullOrEmpty(selectedTable))
            return;

        // Очистка предыдущих полей
        InputFieldsLayout.Children.Clear();
        _inputFields.Clear();

        // Создание полей для ввода данных в зависимости от выбранной таблицы
        switch (selectedTable)
        {
            case "Codes":
                AddInputField("Title", "Название");
                AddInputField("Description", "Описание");
                AddInputField("NameOs", "Операционная система");
                break;
            case "Errors":

                AddInputField("Title", "Название ошибки");
                AddInputField("Description", "Описание");
                break;
            case "Installations":
                AddInputField("Title", "Название");
                AddInputField("Description", "Описание");
                AddInputField("NameOs", "Название Установленной ОС");
                AddInputField("WhereInstall", "Способ установки");
                break;
            case "Fixes":
                AddInputField("Title", "Название");
                AddInputField("Description", "Описание");
                AddInputField("ErrorName", "Название ошибки");
                break;

            case "Updates":
                AddInputField("Title", "Название");
                AddInputField("Description", "Описание");
                break;
        }
    }

    // Добавление поля для ввода данных
    private void AddInputField(string fieldName, string labelText)
    {
        var label = new Label { Text = labelText };
        var entry = new Entry();

        InputFieldsLayout.Children.Add(label);
        InputFieldsLayout.Children.Add(entry);

        _inputFields[fieldName] = entry;
    }

    // Обработчик кнопки "Добавить данные"
    private async void AddDataButton_Clicked(object sender, EventArgs e)
    {
        var selectedTable = TablePicker.SelectedItem as string;
        if (string.IsNullOrEmpty(selectedTable))
        {
            await DisplayAlert("Ошибка", "Выберите таблицу", "OK");
            return;
        }

        try
        {
            switch (selectedTable)
            {
                case "Codes":
                    var code = new Code(
                        _inputFields["Title"].Text,
                        _inputFields["Description"].Text, 
                        _inputFields["NameOs"].Text);
                    _db.Codes.Add(code);
                    break;

                case "Errors":
                    var error = new Error
                    {
                        Title = _inputFields["Title"].Text,
                        Description = _inputFields["Description"].Text,
                    };
                    _db.Errors.Add(error);
                    break;

                case "Fixes":
                    var fix = new Fix
                    {
                        Title = _inputFields["Title"].Text,
                        Description = _inputFields["Description"].Text,
                        ErrorName = _inputFields["ErrorName"].Text,
                        DateAdd = DateTime.Now,
                        DateUpdate = DateTime.Now,
                      
                    };
                    _db.Fixes.Add(fix);
                    break;

                case "Installations":
                    var installation = new Installation
                    {
                        Title = _inputFields["Title"].Text,
                        Description = _inputFields["Description"].Text,
                        NameOs = _inputFields["NameOs"].Text,
                        WhereInstall = _inputFields["WhereInstall"].Text,
                        Create_dt = DateTime.Now,
                        Update_dt = DateTime.Now,

                    };
                    _db.Installations.Add(installation);
                    break;

                case "Updates":
                    var update = new Update
                    {
                        Title = _inputFields["Title"].Text,
                        Description = _inputFields["Description"].Text,
                        DateUpdate = DateTime.Now

                    };
                    _db.Updates.Add(update);
                    break;
            }

            await _db.SaveChangesAsync();
            await DisplayAlert("Успех", "Данные успешно добавлены", "OK");

            // Очистка полей после добавления
            foreach (var entry in _inputFields.Values)
            {
                entry.Text = string.Empty;
            }
        }
        catch (Exception ex)
        {
           string errorMessage = $"Ошибка: {ex.Message}";
    if (ex.InnerException != null)
    {
        errorMessage += $"\nInner: {ex.InnerException.Message}";
    }
    await DisplayAlert("Ошибка", errorMessage, "OK");
        }
    }
}

Вот фото как это выглядит сейчас:

введите сюда описание изображения

Страница добавления,но тут я еще не сделал возможность добавления изображения(если, что это старый их вид):

введите сюда описание изображения

Просто хочется попробовать сделать страницу статей в таком виде(см. картинку ниже),но все уперлось в то, как реализовать возможность пользователю добавлять изображения в статьи:

введите сюда описание изображения

Буду рад хоть каким-то идеям как это можно реализовать или хотябы ссылки на похожую статью. Ибо я не смог найти


Ответы (1 шт):

Автор решения: EvgeniyZ

Очень странный вопрос, который я даже не понял... Но раз просят показать пример, чтож, покажу... Единственное, это будет WPF проект, ибо ставить MAUI не хочу, а с WPF они схожи за исключением ряда моментов и компонентов. Пример будет максимально простым, без базы и чего-либо еще (это уж сами).

  1. Возьму ваш класс Installation, вырежу из него лишнее и создам локальный список с 20 случайно сгенерированными данными (библиотека Bogus). Это будет неким аналогом базы данных.

  2. Создам в XAML простейший шаблон отображения, все +- тоже самое, что и у вас, за исключением того, что CollectionView в WPF зовется ItemsControl. Получаем такое:

     <ScrollViewer>
         <ItemsControl x:Name="itemsControl">
             <ItemsControl.ItemTemplate>
                 <DataTemplate>
                     <Grid Margin="5">
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="100" />
                             <ColumnDefinition />
                         </Grid.ColumnDefinitions>
    
                         <Image Grid.Column="0" Source="{Binding ImageUri}" />
    
                         <StackPanel Grid.Column="1" Margin="5 0 0 0">
                             <TextBlock Text="{Binding Title}" FontWeight="Medium" />
                             <TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
                         </StackPanel>
                     </Grid>
                 </DataTemplate>
             </ItemsControl.ItemTemplate>
         </ItemsControl>
     </ScrollViewer>
    

    Как видите, я задал x:Name, как и вы... Не буду использовать привязки правильно, буду дергать контролы через код. Собственно весь код будет такой:

     public class Installation
     {
         public int Id { get; set; }
         public string Title { get; set; }
         public string Description { get; set; }
         public string ImageUri { get; set; }
     }
    
    
    
     public partial class MainWindow : Window
     {
         public List<Installation> Installations { get; set; } = new List<Installation>();
    
         public MainWindow()
         {
             InitializeComponent();
    
             for (var i = 0; i < 20; i++)
             {
                 var installation = new Faker<Installation>()
                         .RuleFor(o => o.Id, f => i)
                         .RuleFor(o => o.Title, f => f.Lorem.Sentence(3))
                         .RuleFor(o => o.Description, f => f.Lorem.Paragraph(2))
                         .RuleFor(o => o.ImageUri, f => f.Image.PicsumUrl())
                         .Generate();
                 Installations.Add(installation);
             }
    
             itemsControl.ItemsSource = Installations;
         }
     }
    

Запускаем и смотрим результат:

Result 1

Как видите, без каких либо конверторов все отобразилось, почти все картинки (там проблема генерации и сайта), в ImageUri находится просто ссылка на картинку (например: https://picsum.photos/640/480/?image=650). Сама ссылка может быть и на локальный файл (скажем, рядом с проектом будет папка Images и там image1.jpg, значит ссылка будет /images/image1.jpg).

Теперь предположим, что нам надо изменить картинку по кнопке. Я с вашего позволения сделаю изменение на случайную.

  1. Добавляю кнопку (у меня под описанием будет).

     <Button Content="Изменить изображение" Click="Button_Click"/>
    
  2. В обработчике клика получаем текущий объект и задаем новый путь до картинки. Так, как мы кастыльно делаем все, без привязок, то ищем "Сендера" (тот, кто отправил событие) и берем его DataContext (источник данных, который в MAUI зовется BindingContext), это и будет привязанный класс.

     private void Button_Click(object sender, RoutedEventArgs e)
     {
         var button = (sender as Button);
         var data = button?.DataContext as Installation;
         var fakeImage = new Faker().Image.PicsumUrl();
    
         if (data is null) return;
         data.ImageUri = fakeImage;
     }
    
  3. Запускаем и... Ничего не происходит, хотя по отладке данные меняются. Все дело в том, что интерфейс надо оповестить, о том, что данные изменились. За это отвечает интерфейс INotifyProprtyChanged, который надо реализовать изменяемому классу, и метод которого надо вызвать у изменяемого свойства. Способов реализовать это полно, в интернете найдете удобный для вас. А я использую современный подход с использованием библиотеки CommunityToolkit.MVVM, который за меня сгенерирует все нужное. Класс тогда превратиться в такой:

     public partial class Installation : ObservableObject
     {
         public int Id { get; set; }
         public string Title { get; set; }
         public string Description { get; set; }
    
         [ObservableProperty]
         private string _imageUri;
     }
    

Запускаем и смотрим результат:

Image Change

Как видите, все работает, без конвертеров и чего-либо еще. Простая работа с данными, простое изменение ссылки.


Для полноты картины расскажу как правильно делать все это.

  1. Задаем окну источник данных на один конкретный класс (обычно его называют MainViewModel или что-то такое. Да да, MVVM. Его не стоит бояться, его обязательно нужно учить с самого начала! В MAUI за источник данных отвечает свойство BindingContext, значит и пишем BindingContext = new MainViewModel();.

  2. Данные для привязки (коллекцию) делаем публичным свойством. В моем примере это уже сделано.

  3. Удаляем itemsControl.ItemsSource = Installations; и переносим это в XAML, прописав там <... ItemsSource = "{Binding Installations}">. Также удаляем x:Name (в нормальном проекте его быть не должно, только если в пределах XAML используется).

  4. Click меняем на команду (Command), привязав к свойству с типом ICommand. Также данные передаем параметрами. В итоге кнопка будет такой:

     <Button Content="Изменить изображение" Command="{Binding ChangeImageCommand}" CommandParameter="{Binding}" />
    

    Но тут есть проблема. Кнопка внутри коллекции, а значит источником данных будет являться конкретный объект коллекции, а не основная VM. Чтобы это обойти, надо найти предка и через него выйти к нужному классу данных. Для MAUI есть отличная документация, которая это все показывает.

  5. Сама команда будет простым методом с аргументом самого объекта, который меняется. При использовании CommunityToolkit.MVVM так и вовсе, это превратиться в

     [RelayCommand]
     private void ChangeImage(Installation installation)
     {
         installation.ImageUri = ...;
     }
    

На этом, пожалуй все. Удачи в изучении!

→ Ссылка