Search code examples
c#data-bindingavaloniaui

Why does binding Text to TextBox not work in Avalonia?


I am new to MVVM and Avalonia and learn it by watching a course on YouTube to create an explorer app. So far I created a super simple explorer like this:
enter image description here
Here is the MainWindow.axaml:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:AvaloniaMVVMExplorer.ViewModels"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
        xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="AvaloniaMVVMExplorer.Views.MainWindow"
        Icon="/Assets/avalonia-logo.ico"
        Title="AvaloniaMVVMExplorer"
        WindowState="Maximized"
        WindowStartupLocation="CenterScreen">

    <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>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        
        <TextBox Grid.Row="0" 
                 Text="{Binding CurrentFilePath}"/>

        <ListBox Grid.Row="1" 
                 x:Name="PathsLB"
                 Items="{Binding DirectoriesAndFiles}" 
                 SelectedItem="{Binding SelectedFileEntity}">
            <i:Interaction.Behaviors>
                <ia:EventTriggerBehavior EventName="DoubleTapped">
                    <ia:InvokeCommandAction Command="{Binding OpenCommand}"
                                            CommandParameter="{Binding ElementName=PathsLB, Path=SelectedItem}"/>
                </ia:EventTriggerBehavior>
            </i:Interaction.Behaviors>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid >
                        <TextBlock Text="{Binding Name}"/>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

</Window>

And here is MainWIndowViewModel.cs:

using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using AvaloniaMVVMExplorer.ViewModels.Base;
using AvaloniaMVVMExplorer.ViewModels.Commands;
using AvaloniaMVVMExplorer.ViewModels.FileViewModels;
using AvaloniaMVVMExplorer.ViewModels.FileViewModels.Base;

namespace AvaloniaMVVMExplorer.ViewModels
{
    internal class MainWindowViewModel : ViewModelBase
    {
        #region Properties
        private string? currentFilePath;
        public string? CurrentFilePath
        {
            get { return currentFilePath; } 
            set { currentFilePath = value; OnPropertyChanged(); }
        }

        private ObservableCollection<FileEntityViewModel>? directoriesAndFiles;
        public ObservableCollection<FileEntityViewModel>? DirectoriesAndFiles
        {
            get { return directoriesAndFiles; }
            set { directoriesAndFiles = value; OnPropertyChanged(); }
        }

        private FileEntityViewModel? selectedFileEntity;
        public FileEntityViewModel? SelectedFileEntity
        {
            get { return selectedFileEntity; }
            set { selectedFileEntity = value; OnPropertyChanged(); }
        }
        #endregion

        #region Commands
        public ICommand OpenCommand { get; }
        #endregion

        #region Constructor
        public MainWindowViewModel()
        {
            DirectoriesAndFiles = new ObservableCollection<FileEntityViewModel>();

            OpenCommand = new DelegateCommand(Open);

            foreach (var logicalDrive in Directory.GetLogicalDrives())
            {
                DirectoriesAndFiles.Add(new DirectoryViewModel(logicalDrive));
            }
        }
        #endregion

        #region CommandMethods
        private void Open(object parameter)
        {
            if (parameter is DirectoryViewModel directoryViewModel)
            {
                CurrentFilePath = directoryViewModel.FullName;

                DirectoriesAndFiles?.Clear();

                var directoryInfo = new DirectoryInfo(CurrentFilePath ?? "");

                foreach (var directory in directoryInfo.GetDirectories()) 
                {
                    DirectoriesAndFiles?.Add(new DirectoryViewModel(directory));
                }

                foreach (var file in directoryInfo.GetFiles())
                {
                    DirectoriesAndFiles?.Add(new FileViewModel(file));
                }
            }
        }
        #endregion
    }
}

Every Binding works fine except this one <TextBox Grid.Row="0" Text="{Binding CurrentFilePath}"/>. The binding is to show the user current path, but the Text of TextBox doesn't change even if CurrentFilePath is changed.
What could be the reason of this? What am I doing wrong? Thanks!
Full project: https://github.com/CrackAndDie/Avalonia-MVVM-Explorer


Solution

  • The problem lies with your PropertyChanged notification.

        internal class ViewModelBase : ReactiveObject
        {
            #region PropetryChangedHandler
            public event PropertyChangedEventHandler? BasePropertyChanged;
    
            protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
            {
                BasePropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
            #endregion
        }
    

    Nothing happens because ReactiveObject already implements INotifyPropertyChanged which WPF/Avalonia looks for which using Bind.

    You must either change your view model to simply Implement INotifyPropertyChanged or change directly in your ViewModel to use ReactiveObject methods set => this.RaiseAndSetIfChanged(ref _memberField, value);

    Since your ViewModel implements ReactiveObject it must use the operations provided by it to trigger change notifications. ReactiveObject is a class provided by ReactiveUI. ReactiveUI can be quite valuable to learn as it does provide many useful mechanisms to keep your code concise while implementing MVVM. However ReactiveUI is built on rx.net, understanding Rx.net is advisiable to use ReactiveUI, but can be a steep learning curve to start with.

    I suggest Implementing INotifyPropertyChanged, which would result in the following in your ViewModelBase:

        internal class ViewModelBase  : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler? PropertyChanged;
    
            [NotifyPropertyChangedInvocator]
            protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    

    If you stick to the ReactiveUI route then you would change:

            public string? CurrentFilePath
            {
                get { return currentFilePath; } 
                set { currentFilePath = value; OnPropertyChanged(); }
            }
    

    To:

            public string? CurrentFilePath
            {
                get => currentFilePath;
                set => this.RaiseAndSetIfChanged(ref currentFilePath, value);
            }