НейроАгент

Не работает привязка в Avalonia UI: причины и решения

Устраните проблемы с привязкой данных в Avalonia UI. Узнайте, почему не обновляется UI при использовании Community MVVM Toolkit и как правильно реализовать MVVM паттерн.

Вопрос

Почему не работает обратная привязка в Avalonia UI?

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

При этом команды с кнопок выполняются и в дебаггере видно изменения значений полей. Однако никаких изменений в UI нет.

Вот мой код:

ViewModel (MainWindowViewModel.cs):

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--;
    }
}

XAML (MainWindow.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>

Где ошибка? Как правильно это сделать?

НейроАгент

Проблема с обновлением UI в Avalonia UI при использовании Community MVVM Toolkit возникает из-за неправильной привязки свойств в XAML и некорректного использования атрибута [ObservableProperty]. Давайте разберем и исправим ваш код.

Ваши значения не обновляются в UI, потому что вы привязываетесь к приватным полям вместо сгенерированных свойств, а также не уведомляете об изменении некоторых значений.


Contents


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

  1. Неправильные привязки в XAML: Вы привязываетесь к приватным полям (_Counts, _Chislo) вместо сгенерированных публичных свойств (Counts, Chislo)

  2. Отсутствие уведомлений об изменении: Свойство RandomValue не реализует INotifyPropertyChanged

  3. Прямое присвоение значений полей: Вместо использования сгенерированных свойств вы изменяете приватные поля напрямую

  4. Некорректная работа с командами: Не используется RelayCommand из Community MVVM Toolkit


Исправление ViewModel

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] создает публичное свойство Counts
    [ObservableProperty]
    private int counts; // Приватное поле с маленькой буквы
    
    [ObservableProperty]
    private int chislo; // Приватное поле с маленькой буквы
    
    // Это свойство также должно поддерживать уведомления
    [ObservableProperty]
    private int randomValue;
    
    [ObservableProperty]
    private string message;
    
    // Команды должны быть реализованы через RelayCommand
    [RelayCommand]
    private void GenerateRandom()
    {
        Random rnd = new Random();
        RandomValue = rnd.Next(1, 10);
        IsCountChange = false;
        IsAttempt = true;
    }

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

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

        Counts--;
    }
}

Ключевые изменения:

  • Переименовал _Countscounts и _Chislochislo (с маленькой буквы)
  • Добавил [ObservableProperty] для randomValue
  • Использовал [RelayCommand] вместо прямого создания команд
  • Изменил обращения к свойствам в методах (например, RandomValue вместо RandomValue)

Исправление 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>
        <!-- Это только для предпросмотра в IDE -->
        <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>
                <!-- Correct binding to public property Counts -->
                <TextBox FontSize="21" Width="200" Margin="5" Text="{Binding Counts}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IsAttempt}">
                <TextBlock FontSize="21" TextAlignment="Right" Margin="5" Text="Введите ваш вариант"></TextBlock>
                <!-- Correct binding to public property Chislo -->
                <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>
                <!-- This binding was already correct -->
                <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>

Основные исправления:

  • Убрал Mode=TwoWay из IsVisible (этот атрибут не поддерживает TwoWay)
  • Изменил привязки: {Binding Counts} и {Binding Chislo} вместо _Counts и _Chislo
  • Исправил команды на {Binding GenerateRandomCommand} и {Binding CheckInputCommand}

Важные моменты работы с MVVM

1. Правила работы с ObservableProperty

csharp
// Правильно - [ObservableProperty] создает публичное свойство
[ObservableProperty]
private string userName; // Приватное поле

// Генерируется автоматически:
public string UserName 
{
    get => userName;
    set => SetProperty(ref userName, value);
}

2. Правильные привязки в XAML

xml
<!-- Привязка к сгенерированному свойству -->
<TextBlock Text="{Binding UserName}" />

<!-- Привязка к команде -->
<Button Command="{Binding SaveCommand}" />

3. Установка DataContext

csharp
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        
        // Установка DataContext в коде
        DataContext = new MainWindowViewModel();
    }
}

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

ViewModel

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]
    private int counts;
    
    [ObservableProperty]
    private int chislo;
    
    [ObservableProperty]
    private int randomValue;
    
    [ObservableProperty]
    private string message;
    
    [RelayCommand]
    private void GenerateRandom()
    {
        Random rnd = new Random();
        RandomValue = rnd.Next(1, 10);
        IsCountChange = false;
        IsAttempt = true;
    }

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

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

        Counts--;
    }
}

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">

    <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}">
                <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 GenerateRandomCommand}"></Button>
            <Button FontSize="21" HorizontalAlignment="Stretch" Content="Проверить удачу" IsVisible="{Binding IsAttempt}"
                    Command="{Binding CheckInputCommand}"></Button>
        </StackPanel>
    </Grid>

</Window>

Заключение

  1. Основная проблема была в неправильных привязках - вы использовали имена приватных полей вместо сгенерированных публичных свойств

  2. Community MVVM Toolkit автоматически создает свойства из полей с атрибутом [ObservableProperty], следуя правилу:

    • Поле: private int counts;
    • Свойство: public int Counts { get; set; }
  3. Всегда используйте сгенерированные свойства в коде и XAML, а не приватные поля

  4. Команды должны создаваться через атрибут [RelayCommand], что автоматически сгенерирует свойства с суффиксом Command

  5. Убедитесь, что DataContext установлен правильно в коде конструктора окна

После этих исправлений ваш UI должен корректно реагировать на изменения значений в ViewModel.