НейроАгент

Почему не работает биндинг в Avalonia UI

Решение проблем с обратной привязкой в Avalonia UI при использовании CommunityToolkit.Mvvm. Узнайте, как правильно реализовать MVVM паттерн и исправить обновление UI.

Вопрос

Почему не работает обратная привязка в Avalonia UI? Я пытаюсь разобраться с паттерном MVVM и Avalonia UI в частности. Для реализации Notify решил использовать Community MVVM Toolkit. Создал [ObservableProperty]. Однако значения в UI не изменяются. Дефолтные биндинги работают, а вот присвоенные из кода значения не отображаются. При этом команды с кнопок выполняются и в дебаггере видно изменения значений полей. Однако никаких изменений в UI нет. Где ошибка? Как правильно это сделать?

Вот мой код:

MainWindowsViewModel

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

namespace PrakN.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
    [ObservableProperty]
    private bool isCountChange = true;
    [ObservableProperty]
    private bool isAttempt = false;
    [ObservableProperty]
    private bool isAttemptAllow;
    [ObservableProperty]
    public int _Counts;
    [ObservableProperty]
    public int _Chislo;
    public int RandomValue { get; private set; }
    [ObservableProperty]
    private string _Message;
    
    public void GenerateRandow()
    {
        Random rnd = new Random();
        RandomValue = rnd.Next(1, 10);
        isCountChange = false;
        isAttempt = true;
    }

    public void CheckInput()
    {
        if (Chislo == RandomValue)
        {
            Message = "Вы угадали";
            return;
        }

        if (Counts == 0)
        {
            isAttemptAllow = false;
            return;
        }

        Counts--;
    }
}

MainWindows.xaml

xml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:PrakN.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="PrakN.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="PrakN">

    <Design.DataContext>
        <!-- This only sets the DataContext for the previewer in an IDE,
             to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <Grid>
        <StackPanel HorizontalAlignment="Center">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsEnabled="{Binding IsCountChange }">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Введите число попыток"></TextBlock>
                <TextBox FontSize="21" Width="200" Margin="5" Text="{Binding Counts}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IsAttempt, Mode=TwoWay}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Введите ваш вариант"></TextBlock>
                <TextBox FontSize="21" Width="200" Margin="5" Text="{Binding Chislo}" IsReadOnly="{Binding IsAttemptAllow}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IsAttempt}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Сообщение пользователю"></TextBlock>
                <TextBlock FontSize="21" Width="200" Margin="5" Text="{Binding Message}"></TextBlock>
            </StackPanel>
            <Button FontSize="21" HorizontalAlignment="Stretch" Content="Установить число попыток" IsVisible="{Binding IsCountChange}"
                    Command="{Binding GenerateRandow}"></Button>
            <Button FontSize="21" HorizontalAlignment="Stretch" Content="Проверить удачу" IsVisible="{Binding IsAttempt, Mode=TwoWay}"
                    Command="{Binding CheckInput}"></Button>
        </StackPanel>
    </Grid>

</Window>
НейроАгент

Проблема с обратной привязкой в вашем коде связана с несколькими ключевыми моментами в реализации MVVM с использованием Community MVVM Toolkit в Avalonia UI.

Основная причина, по которой UI не обновляется при изменении значений из кода, заключается в том, что ваш ViewModelBase, скорее всего, не правильно реализует интерфейс INotifyPropertyChanged или не использует необходимые базовые классы из CommunityToolkit.Mvvm.

Содержание

Основные проблемы в вашем коде

  1. Наследование от неправильного базового класса
    Ваш MainWindowViewModel наследуется от ViewModelBase, но для работы с [ObservableProperty] необходимо наследоваться от ObservableObject из CommunityToolkit.Mvvm.

  2. Неправильное именование полей
    Для атрибута [ObservableProperty] имена полей должны соответствовать соглашениям naming convention, иначе генератор кода не сможет правильно создать свойства.

  3. Отсутствие правильной реализации INotifyPropertyChanged
    Без правильного базового класса система не сможет уведомлять UI об изменениях свойств.

Правильная реализация ViewModel

Вам нужно исправить базовый класс и реализацию ViewModel:

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

namespace PrakN.ViewModels;

// Наследуемся от ObservableObject, а не от ViewModelBase
public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty]
    private bool _isCountChange = true;
    
    [ObservableProperty]
    private bool _isAttempt = false;
    
    [ObservableProperty]
    private bool _isAttemptAllow;
    
    [ObservableProperty]
    private int _counts;
    
    [ObservableProperty]
    private int _chislo;
    
    public int RandomValue { get; private set; }
    
    [ObservableProperty]
    private string _message;
    
    [RelayCommand]
    public void GenerateRandom()
    {
        Random rnd = new Random();
        RandomValue = rnd.Next(1, 10);
        IsCountChange = false;
        IsAttempt = true;
        // Эти изменения должны автоматически уведомить UI
    }

    [RelayCommand]
    public void CheckInput()
    {
        if (Chislo == RandomValue)
        {
            Message = "Вы угадали";
            return;
        }

        if (Counts == 0)
        {
            IsAttemptAllow = false;
            return;
        }

        Counts--;
        // Это изменение также должно обновить UI
    }
}

Важно: Обратите внимание, что я переименовал методы и свойства, чтобы соответствовать соглашениям C# (PascalCase для публичных членов).

Настройка биндингов в XAML

Ваш XAML в целом правильный, но есть несколько улучшений:

xml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:PrakN.ViewModels"
        xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="PrakN.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="PrakN">

    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <Grid>
        <StackPanel HorizontalAlignment="Center">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsEnabled="{Binding IsCountChange}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Введите число попыток"></TextBlock>
                <TextBox FontSize="21" Width="200" Margin="5" Text="{Binding Counts, Mode=TwoWay}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IsAttempt}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Введите ваш вариант"></TextBlock>
                <TextBox FontSize="21" Width="200" Margin="5" Text="{Binding Chislo, Mode=TwoWay}" IsReadOnly="{Binding IsAttemptAllow}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IsAttempt}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Сообщение пользователю"></TextBlock>
                <TextBlock FontSize="21" Width="200" Margin="5" Text="{Binding Message}"></TextBlock>
            </StackPanel>
            <Button FontSize="21" HorizontalAlignment="Stretch" Content="Установить число попыток" 
                    IsVisible="{Binding IsCountChange}" Command="{Binding GenerateRandomCommand}"></Button>
            <Button FontSize="21" HorizontalAlignment="Stretch" Content="Проверить удачу" 
                    IsVisible="{Binding IsAttempt}" Command="{Binding CheckInputCommand}"></Button>
        </StackPanel>
    </Grid>

</Window>

Решение распространенных проблем

1. Проблема с DataContext

Убедитесь, что в App.axaml.cs или конструкторе MainWindow вы правильно устанавливаете DataContext:

csharp
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        
        // Правильная установка DataContext
        this.DataContext = new MainWindowViewModel();
    }
}

2. Проблема с генерацией кода

Убедитесь, что у вас установлены необходимые пакеты NuGet:

  • CommunityToolkit.Mvvm
  • Avalonia

И что в файле .csproj включен генератор исходного кода:

xml
<ItemGroup>
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
    <PackageReference Include="Avalonia" Version="11.0.10" />
</ItemGroup>

3. Проблема с потоками

Если изменения происходят в потоке отличном от UI-потока, используйте Dispatcher:

csharp
[RelayCommand]
public void GenerateRandom()
{
    Task.Run(() =>
    {
        Random rnd = new Random();
        var newValue = rnd.Next(1, 10);
        
        Avalonia.Threading.UIThread.MainDispatcher.Post(() =>
        {
            RandomValue = newValue;
            IsCountChange = false;
            IsAttempt = true;
        });
    });
}

Дополнительные рекомендации

  1. Используйте [RelayCommand] для команд вместо публичных методов
  2. Проверьте режимы биндинга: для TextBox используйте Mode=TwoWay, для TextBlock - OneWay
  3. Добавьте отладку биндингов в Avalonia для диагностики проблем
  4. Используйте nameof() для повышения надежности кода
csharp
// Вместо жестко закодированных строк
OnPropertyChanged("Counts");

// Используйте nameof()
OnPropertyChanged(nameof(Counts));

Полный исправленный код

MainWindowViewModel.cs:

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

namespace PrakN.ViewModels;

public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty]
    private bool _isCountChange = true;
    
    [ObservableProperty]
    private bool _isAttempt = false;
    
    [ObservableProperty]
    private bool _isAttemptAllow;
    
    [ObservableProperty]
    private int _counts;
    
    [ObservableProperty]
    private int _chislo;
    
    public int RandomValue { get; private set; }
    
    [ObservableProperty]
    private string _message;

    public MainWindowViewModel()
    {
        // Инициализация по умолчанию
        Counts = 3; // Например, 3 попытки по умолчанию
    }

    [RelayCommand]
    public void GenerateRandom()
    {
        Random rnd = new Random();
        RandomValue = rnd.Next(1, 10);
        IsCountChange = false;
        IsAttempt = true;
        Message = "Угадайте число от 1 до 10";
    }

    [RelayCommand]
    public void CheckInput()
    {
        if (Chislo == RandomValue)
        {
            Message = "Вы угад!";
            IsAttempt = false;
            return;
        }

        if (Counts <= 1)
        {
            IsAttemptAllow = false;
            Message = $"Вы не угадали. Загаданное число было {RandomValue}";
            return;
        }

        Counts--;
        Message = $"Неверно. Осталось попыток: {Counts}";
    }
}

Заключение

Основные проблемы в вашем коде были:

  1. Неправильное наследование - нужно использовать ObservableObject вместо ViewModelBase
  2. Отсутствие атрибутов [RelayCommand] - команды должны быть помечены этим атрибутом
  3. Потенциальные проблемы с DataContext - убедитесь, что он правильно установлен

После исправления этих проблем привязки должны работать корректно, и UI будет обновляться при изменении свойств в ViewModel. Если проблема останется, проверьте наличие ошибок в окне “Output” Visual Studio и убедитесь, что генератор исходного кода CommunityToolkit.Mvvm работает правильно.