Do WPF list based controls support two-way binding to Realm-dotnet live queries?

135 views Asked by At

This is my first question on stack overflow.

I'm trying to replace LiteDB with Realm DB in my WPF application.

My goal is to bind ListBoxes and DataGrids to live Queries in my view models, so as UI elements reflects database state after database operations.

My App behave as expected when I add objects to Realm database, and the list controls are updated as expected as RealmResults is supposed to be Observable. But, when I try to delete an object from the database, the list controls are not automatically refreshed, and an exception is thrown.

I adapted my sample app to WinUI 3 and the delete operation behaves as expected (ie UI listbox items deleted accordingly) .I thought then that WPF does not play well with realm-dotnet.

Is there something I can do beside reassigning ItemsSource each time I remove an object from the database?

Here is a sample that demonstrates the issue (my real app is quite large to be pasted here)

MainWindowViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MongoDB.Bson;
using Realms;
using Realms.Exceptions;

namespace RealmDb
{
    public partial class MainWindowViewModel : ObservableObject
    {
        public MainWindowViewModel() 
        {
            var config = new RealmConfiguration(@"d:\temp\cats.realm");

            try
            {
                Realm.DeleteRealm(config);
            }
            catch(Exception ex){ }

            LocalRealm = Realm.GetInstance(config);

            Cats =  LocalRealm.All<Cat>();
        }

        public Realm LocalRealm;

       
        private IQueryable<Cat> _cats;
        public IQueryable<Cat> Cats
        {
            get => _cats;
            set => SetProperty(ref _cats, value);
        }
   
        public Cat? SelectedCat { get; set; }

       
        [RelayCommand]
        private void RemoveCat()
        {
            if (SelectedCat == null)
                return; 
    
            try
            {
                // throws exception: Attempting to access an invalid object
                LocalRealm.Write(() =>
                {
                    LocalRealm.Remove(SelectedCat);

                });

                /*
                 Workaround: 
                Cats = null;
                Cats = LocalRealm.All<Cat>();*/
            }
            catch (Exception ex)
            {
                // the selected cat is not removed from the datagrid.
                // In the case of binding to a listbox. The deleted object still shows but it rises exception if selected.
                MessageBox.Show($@"Error deleting the selected cat. {ex.Message}");
            }
        }

        [RelayCommand]
        private void Populate()
        {
            Cat ninja = new() { Name = "Ninja", Age = 1, Breed = "Angora" };
            Cat nounou = new() { Name = "Nounou", Age = 2, Breed = "Siamese" };
            Cat leila = new() { Name = "Layla", Age = 3, Breed = "Local" };

            try
            {
                // grouped writes trows "Range actions are not supported"
                LocalRealm.Write(() =>
                {
                    LocalRealm.Add(nounou);      
                });

                LocalRealm.Write(() =>
                {
                    LocalRealm.Add(ninja);
                });

                LocalRealm.Write(() =>
                {
                    LocalRealm.Add(leila);
                });
               
            }
            catch (RealmFileAccessErrorException ex)
            {
                MessageBox.Show($@"Error writeing to the realm file. {ex.Message}");
            }
        }

    }
}


MainWindow.xaml

<Window x:Class="RealmDb.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RealmDb"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        DataContext="{Binding Source={x:Static local:App.MainViewModel}}">
    <StackPanel>
        <Button Content="Populate" Command="{Binding PopulateCommand}" />
        <Button Content="Remove" Command="{Binding RemoveCatCommand}" />
        <DataGrid Name="dataGrid"
                  AutoGenerateColumns="False"
                  ItemsSource="{Binding Cats, Mode=TwoWay}"
                  SelectedItem="{Binding SelectedCat, Mode=TwoWay}" >
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
                <DataGridTextColumn Header="Age" Binding="{Binding Age}"/>
                <DataGridTextColumn Header="Breed" Binding="{Binding Breed}"/>
            </DataGrid.Columns>
        </DataGrid>

    </StackPanel>
</Window>


Cat.cs

public partial class Cat : IRealmObject
{
    [PrimaryKey]
    public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; } = 0;
    public string Breed { get; set; } = string.Empty;
    
}

App.xaml.cs

 using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Data;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Windows;
    
    namespace RealmDb
    {
        /// <summary>
        /// Interaction logic for App.xaml
        /// </summary>
        public partial class App : Application
        {
            private static MainWindowViewModel? _mainWindowsViewModel;
            public static MainWindowViewModel MainViewModel => _mainWindowsViewModel ??= new MainWindowViewModel();
    
        }
    }
1

There are 1 answers

1
Nikola Irinchev On BEST ANSWER

Unfortunately, two-way automatic data-binding is not supported in WPF apps like it is for MAUI/Xamarin.Forms apps. This means that changes from the UI will not automatically create transactions to persist the updates in the database - you'll either need to explicitly start a write transaction, or you'll need to create some custom behavior that will start a write transaction if one doesn't exist already, execute the property change, then commit the transaction if necessary.

This snippet from the Realm .NET does roughly this. It integrates with the Xamarin.Forms/MAUI data-binding engine and replaces the RealmObject property setters used by the binding engine to create these implicit transactions.