Better adaptation to reactive programming
Better usage of DynamicData.
ADD
WalletWasabi.Fluent/Extensions/DynamicDataMixin.cs
+using System.Reactive.Linq;
+using DynamicData;
+using DynamicData.Kernel;
+
+namespace WalletWasabi.Fluent.Extensions;
+
+public static class Mixin
+{
+ /// <summary>
+ /// Transforms the items, and when an update is received, allows the preservation of the previous view model
+ /// </summary>
+ /// <typeparam name="TObject">The type of the object.</typeparam>
+ /// <typeparam name="TKey">The type of the key.</typeparam>
+ /// <typeparam name="TDestination">The type of the destination.</typeparam>
+ /// <param name="source">The source.</param>
+ /// <param name="transformFactory">The transform factory.</param>
+ /// <param name="updateAction">Apply changes to the original. Example (previousTransformedItem, newOriginalItem) => previousTransformedItem.Value = newOriginalItem </param>
+ /// <returns></returns>
+ public static IObservable<IChangeSet<TDestination, TKey>> TransformWithInlineUpdate<TObject, TKey, TDestination>(this IObservable<IChangeSet<TObject, TKey>> source,
+ Func<TObject, TDestination> transformFactory,
+ Action<TDestination, TObject> updateAction = null)
+ {
+ return source.Scan((ChangeAwareCache<TDestination, TKey>)null, (cache, changes) =>
+ {
+ if (cache == null)
+ {
+ cache = new ChangeAwareCache<TDestination, TKey>(changes.Count);
+ }
+
+ foreach (var change in changes)
+ {
+ switch (change.Reason)
+ {
+ case ChangeReason.Add:
+ cache.AddOrUpdate(transformFactory(change.Current), change.Key);
+ break;
+ case ChangeReason.Update:
+ {
+ if (updateAction == null)
+ {
+ continue;
+ }
+
+ var previous = cache.Lookup(change.Key).ValueOrThrow(() => new MissingKeyException($"{change.Key} is not found."));
+
+ updateAction(previous, change.Current);
+
+ //send a refresh as this will force downstream operators
+ cache.Refresh(change.Key);
+ }
+ break;
+ case ChangeReason.Remove:
+ cache.Remove(change.Key);
+ break;
+ case ChangeReason.Refresh:
+ cache.Refresh(change.Key);
+ break;
+ case ChangeReason.Moved:
+ //Do nothing !
+ break;
+ }
+ }
+ return cache;
+ }).Select(cache => cache.CaptureChanges());
+ }
+}

MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinsViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinsViewModel.cs
+using System.Collections.Generic;
using System.Collections.Immutable;
-using DynamicData;
-using ReactiveUI;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
@@ -9,8 +8,8 @@ using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Templates;
-using DynamicData.Binding;
-using NBitcoin;
+using DynamicData;
+using ReactiveUI;
using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Fluent.Extensions;
using WalletWasabi.Fluent.ViewModels.Dialogs;
@@ -23,28 +22,92 @@ namespace WalletWasabi.Fluent.ViewModels.Wallets.Advanced.WalletCoins;
[NavigationMetaData(Title = "Wallet Coins (UTXOs)")]
public partial class WalletCoinsViewModel : RoutableViewModel
{
+ [AutoNotify] private ObservableAsPropertyHelper<bool>? _anySelected;
+ [AutoNotify] private FlatTreeDataGridSource<WalletCoinViewModel> _source;
+
private readonly WalletViewModel _walletViewModel;
- private readonly IObservable<Unit> _balanceChanged;
- private readonly ObservableCollectionExtended<WalletCoinViewModel> _coins;
- private readonly SourceList<WalletCoinViewModel> _coinsSourceList = new();
- [AutoNotify] private FlatTreeDataGridSource<WalletCoinViewModel>? _source;
- [AutoNotify] private bool _anySelected;
public WalletCoinsViewModel(WalletViewModel walletViewModel, IObservable<Unit> balanceChanged)
{
+ _walletViewModel = walletViewModel;
SetupCancel(false, true, true);
NextCommand = CancelCommand;
- _walletViewModel = walletViewModel;
- _balanceChanged = balanceChanged;
- _coins = new ObservableCollectionExtended<WalletCoinViewModel>();
SkipCommand = ReactiveCommand.CreateFromTask(OnSendCoins);
}
+ public bool IsAnySelected => _anySelected?.Value ?? false;
+
+ private static int GetOrderingPriority(WalletCoinViewModel x)
+ {
+ if (x.CoinJoinInProgress)
+ {
+ return 1;
+ }
+
+ if (x.IsBanned)
+ {
+ return 2;
+ }
+
+ if (!x.Confirmed)
+ {
+ return 3;
+ }
+
+ return 0;
+ }
+
+ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ {
+ var initial = Observable.Return(GetCoins());
+
+ var polling = Observable
+ .Timer(TimeSpan.FromSeconds(2))
+ .Repeat()
+ .Select(_ => GetCoins());
+
+ var source = initial.Concat(polling).Publish();
+
+ var observable = source
+ .ToObservableChangeSet(c => c.HdPubKey.GetHashCode())
+ .AsObservableCache()
+ .Connect()
+ .TransformWithInlineUpdate(x => new WalletCoinViewModel(x))
+ .Publish();
+
+ _anySelected = observable
+ .AutoRefresh(x => x.IsSelected)
+ .ToCollection()
+ .Select(items => items.Any(t => t.IsSelected))
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .ToProperty(this, model => model.IsAnySelected)
+ .DisposeWith(disposables);
+ this.RaisePropertyChanged(nameof(IsAnySelected));
+
+ observable
+ .DisposeMany()
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Bind(out var coins)
+ .Subscribe()
+ .DisposeWith(disposables);
+
+ Source = CreateGridSource(coins)
+ .DisposeWith(disposables);
+
+ observable.Connect()
+ .DisposeWith(disposables);
+
+ source.Connect()
+ .DisposeWith(disposables);
+
+ base.OnNavigatedTo(isInHistory, disposables);
+ }
+
private async Task OnSendCoins()
{
var wallet = _walletViewModel.Wallet;
- var selectedSmartCoins = _coins.Where(x => x.IsSelected).Select(x => x.Coin).ToImmutableArray();
+ var selectedSmartCoins = _source.Items.Where(x => x.IsSelected).Select(x => x.Coin).ToImmutableList();
var info = new TransactionInfo(wallet.KeyManager.AnonScoreTarget);
var addressDialog = new AddressEntryDialogViewModel(wallet.Network, info);
@@ -67,54 +130,18 @@ public partial class WalletCoinsViewModel : RoutableViewModel
info.UserLabels = label;
info.IsSelectedCoinModificationEnabled = false;
- Navigate().To(new TransactionPreviewViewModel(wallet, info, address, isFixedAmount: true));
+ Navigate().To(new TransactionPreviewViewModel(wallet, info, address, true));
}
- private IObservable<Unit> CoinsUpdated => _balanceChanged
- .ToSignal()
- .Merge(_walletViewModel
- .WhenAnyValue(w => w.IsCoinJoining)
- .ToSignal());
-
- protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ private FlatTreeDataGridSource<WalletCoinViewModel> CreateGridSource(IEnumerable<WalletCoinViewModel> coins)
{
- base.OnNavigatedTo(isInHistory, disposables);
-
- _coinsSourceList
- .Connect()
- .Sort(SortExpressionComparer<WalletCoinViewModel>.Descending(model => model.Amount))
- .ObserveOn(RxApp.MainThreadScheduler)
- .Bind(_coins)
- .DisposeMany()
- .Subscribe()
- .DisposeWith(disposables);
-
- _coinsSourceList
- .Connect()
- .ObserveOn(RxApp.MainThreadScheduler)
- .WhenValueChanged(x => x.IsSelected)
- .Select(_ => _coinsSourceList.Items.Any(x => x.IsSelected))
- .Subscribe(anySelected => AnySelected = anySelected)
- .DisposeWith(disposables);
-
- Observable.Timer(TimeSpan.FromSeconds(30))
- .Subscribe(_ =>
- {
- foreach (var coin in GetCoins())
- {
- coin.RefreshAndGetIsBanned();
- }
- })
- .DisposeWith(disposables);
-
// [Column] [View] [Header] [Width] [MinWidth] [MaxWidth] [CanUserSort]
// Selection SelectionColumnView - Auto - - false
// Indicators IndicatorsColumnView - Auto - - true
// Amount AmountColumnView Amount Auto - - true
// AnonymityScore AnonymityColumnView <custom> 50 - - true
// Labels LabelsColumnView Labels * - - true
-
- Source = new FlatTreeDataGridSource<WalletCoinViewModel>(_coins)
+ var source = new FlatTreeDataGridSource<WalletCoinViewModel>(coins)
{
Columns =
{
@@ -138,7 +165,7 @@ public partial class WalletCoinsViewModel : RoutableViewModel
CanUserResizeColumn = false,
CanUserSortColumn = true,
CompareAscending = WalletCoinViewModel.SortAscending(x => GetOrderingPriority(x)),
- CompareDescending = WalletCoinViewModel.SortDescending(x => GetOrderingPriority(x)),
+ CompareDescending = WalletCoinViewModel.SortDescending(x => GetOrderingPriority(x))
},
width: new GridLength(0, GridUnitType.Auto)),
@@ -177,55 +204,19 @@ public partial class WalletCoinsViewModel : RoutableViewModel
CanUserResizeColumn = false,
CanUserSortColumn = true,
CompareAscending = WalletCoinViewModel.SortAscending(x => x.SmartLabel),
- CompareDescending = WalletCoinViewModel.SortDescending(x => x.SmartLabel),
+ CompareDescending = WalletCoinViewModel.SortDescending(x => x.SmartLabel)
},
- width: new GridLength(1, GridUnitType.Star)),
+ width: new GridLength(1, GridUnitType.Star))
}
};
- disposables.Add(Disposable.Create(() => _coins.Clear()));
+ source.RowSelection!.SingleSelect = true;
- Source.DisposeWith(disposables);
-
- Source.RowSelection!.SingleSelect = true;
-
- CoinsUpdated
- .Select(_ => GetCoins())
- .Subscribe(RefreshCoinsList)
- .DisposeWith(disposables);
- }
-
- private static int GetOrderingPriority(WalletCoinViewModel x)
- {
- if (x.CoinJoinInProgress)
- {
- return 1;
- }
-
- if (x.IsBanned)
- {
- return 2;
- }
-
- if (!x.Confirmed)
- {
- return 3;
- }
-
- return 0;
+ return source;
}
private ICoinsView GetCoins()
{
return _walletViewModel.Wallet.Coins;
}
-
- private void RefreshCoinsList(ICoinsView items)
- {
- _coinsSourceList.Edit(x =>
- {
- x.Clear();
- x.AddRange(items.Select(coin => new WalletCoinViewModel(coin)));
- });
- }
}

MODIFY
WalletWasabi.Fluent/Views/Wallets/Advanced/WalletCoins/WalletCoinsView.axaml
WalletWasabi.Fluent/Views/Wallets/Advanced/WalletCoins/WalletCoinsView.axaml
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SkipContent="Send selected coins"
- EnableSkip="{Binding AnySelected}">
+ EnableSkip="{Binding IsAnySelected}">
<TreeDataGrid Source="{Binding Source}"
CanUserSortColumns="True">
<TreeDataGrid.Styles>

Better adaptation to reactive programming

Better usage of DynamicData.