Refactored flyout show/hide behavior
ADD
WalletWasabi.Fluent/Behaviors/ShowAssociatedFlyoutBehavior.cs
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Xaml.Interactivity;
+
+namespace WalletWasabi.Fluent.Behaviors;
+
+public class ShowAssociatedFlyoutBehavior : Behavior<Control>
+{
+ public static readonly StyledProperty<bool> IsSearchPanelOpenProperty =
+ AvaloniaProperty.Register<ShowAssociatedFlyoutBehavior, bool>(nameof(IsSearchPanelOpen));
+
+ private IDisposable? _isOpenSyncer;
+ private IDisposable? _flyoutToggler;
+
+ public bool IsSearchPanelOpen
+ {
+ get => GetValue(IsSearchPanelOpenProperty);
+ set => SetValue(IsSearchPanelOpenProperty, value);
+ }
+
+ protected override void OnAttachedToVisualTree()
+ {
+ _isOpenSyncer = FlyoutBase.GetAttachedFlyout(AssociatedObject)
+ .GetObservable(FlyoutBase.IsOpenProperty)
+ .Subscribe(b => IsSearchPanelOpen = b);
+
+ _flyoutToggler = this.GetObservable(IsSearchPanelOpenProperty).Subscribe(isOpen =>
+ {
+ if (isOpen)
+ {
+ FlyoutBase.ShowAttachedFlyout(AssociatedObject);
+ }
+ else
+ {
+ FlyoutBase.GetAttachedFlyout(AssociatedObject).Hide();
+ }
+ });
+ }
+
+ protected override void OnDetachedFromVisualTree()
+ {
+ _flyoutToggler?.Dispose();
+ _flyoutToggler?.Dispose();
+
+ base.OnDetachedFromVisualTree();
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/ViewModels/SearchBar/SearchBarDesignViewModel.cs
WalletWasabi.Fluent/ViewModels/SearchBar/SearchBarDesignViewModel.cs
@@ -32,9 +32,9 @@ public class SearchBarDesignViewModel : ReactiveObject
.GroupBy(r => r.Category)
.Select(g =>
{
- var sourceCache = new SourceCache<SearchItem, ComposedKey>(r => r.Key);
+ var sourceCache = new SourceCache<SearchItemViewModel, ComposedKey>(r => r.Key);
var observable = g.ToObservable();
- sourceCache.PopulateFrom(observable);
+ sourceCache.PopulateFrom(observable.Select(item => new SearchItemViewModel(item, () => { })));
return new SearchItemGroup(g.Key, sourceCache);
})));

MODIFY
WalletWasabi.Fluent/ViewModels/SearchBar/SearchBarViewModel.cs
WalletWasabi.Fluent/ViewModels/SearchBar/SearchBarViewModel.cs
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
+using System.Windows.Input;
using DynamicData;
using ReactiveUI;
@@ -8,13 +10,18 @@ namespace WalletWasabi.Fluent.ViewModels.SearchBar;
public class SearchBarViewModel : ReactiveObject
{
- private readonly ReadOnlyObservableCollection<SearchItemGroup> _groups;
+ private readonly ReadOnlyObservableCollection<SearchItemGroup?> _groups;
+ private readonly ObservableAsPropertyHelper<ICommand?> _selectedCommand;
+ private bool _isSearchListVisible;
private string _searchText;
+ private SearchItem _selectedSearchItem;
public SearchBarViewModel(IObservable<SearchItem> itemsObservable)
{
- var source = new SourceCache<SearchItem, ComposedKey>(item => item.Key);
- source.PopulateFrom(itemsObservable);
+ var vms = itemsObservable.Select(item => new SearchItemViewModel(item, () => { IsSearchListVisible = false; }));
+
+ var source = new SourceCache<SearchItemViewModel, ComposedKey>(item => item.Key);
+ source.PopulateFrom(vms);
var filterPredicate = this
.WhenAnyValue(x => x.SearchText)
@@ -31,6 +38,16 @@ public class SearchBarViewModel : ReactiveObject
.DisposeMany()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe();
+
+ ShowListCommand = ReactiveCommand.Create(() => IsSearchListVisible = true);
+ }
+
+ public ReactiveCommand<Unit, bool> ShowListCommand { get; set; }
+
+ public bool IsSearchListVisible
+ {
+ get => _isSearchListVisible;
+ set => this.RaiseAndSetIfChanged(ref _isSearchListVisible, value);
}
public ReadOnlyObservableCollection<SearchItemGroup> Groups => _groups;
@@ -41,7 +58,7 @@ public class SearchBarViewModel : ReactiveObject
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}
- private static Func<SearchItem, bool> SearchItemFilterFunc(string? text)
+ private static Func<SearchItemViewModel, bool> SearchItemFilterFunc(string? text)
{
return searchItem =>
{
@@ -51,7 +68,8 @@ public class SearchBarViewModel : ReactiveObject
}
var containsName = searchItem.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase);
- var containsAnyTag = searchItem.Keywords.Any(s => s.Contains(text, StringComparison.InvariantCultureIgnoreCase));
+ var containsAnyTag =
+ searchItem.Keywords.Any(s => s.Contains(text, StringComparison.InvariantCultureIgnoreCase));
return containsName || containsAnyTag;
};
}

MODIFY
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItem.cs
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItem.cs
using System.Collections.Generic;
using System.Linq;
-using System.Windows.Input;
+using System.Threading.Tasks;
namespace WalletWasabi.Fluent.ViewModels.SearchBar;
public class SearchItem
{
- public ICommand Command { get; }
+ public Func<Task> OnExecution { get; }
- public SearchItem(string name, string description, ICommand command, string category, IEnumerable<string>? keywords = null)
+ public SearchItem(string name, string description, Func<Task> onExecution, string category,
+ IEnumerable<string>? keywords = null)
{
Name = name;
Description = description;
- Command = command;
+ OnExecution = onExecution;
Category = category;
Keywords = keywords ?? Enumerable.Empty<string>();
}

MODIFY
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItemGroup.cs
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItemGroup.cs
@@ -7,9 +7,9 @@ namespace WalletWasabi.Fluent.ViewModels.SearchBar;
public class SearchItemGroup
{
- private readonly ReadOnlyObservableCollection<SearchItem> _items;
+ private readonly ReadOnlyObservableCollection<SearchItemViewModel> _items;
- public SearchItemGroup(string title, IObservableCache<SearchItem, ComposedKey> groupCache)
+ public SearchItemGroup(string title, IObservableCache<SearchItemViewModel, ComposedKey> groupCache)
{
Title = title;
groupCache.Connect()
@@ -21,5 +21,5 @@ public class SearchItemGroup
public string Title { get; }
- public ReadOnlyObservableCollection<SearchItem> Items => _items;
+ public ReadOnlyObservableCollection<SearchItemViewModel> Items => _items;
}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItemProvider.cs
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItemProvider.cs
using System.Linq;
-using System.Reactive;
using System.Reactive.Linq;
-using ReactiveUI;
+using System.Threading.Tasks;
using WalletWasabi.Fluent.ViewModels.Navigation;
namespace WalletWasabi.Fluent.ViewModels.SearchBar;
@@ -14,10 +13,10 @@ public static class SearchItemProvider
.Where(m => m.Searchable)
.Select(m =>
{
- var command = CreateCommand(m);
- var searchItem = new SearchItem(m.Title, m.Caption, command, m.Category ?? "No category", m.Keywords)
+ var func = CreateFunc(m);
+ var searchItem = new SearchItem(m.Title, m.Caption, func, m.Category ?? "No category", m.Keywords)
{
- Icon = m.IconName,
+ Icon = m.IconName
};
return searchItem;
});
@@ -27,17 +26,18 @@ public static class SearchItemProvider
return items;
}
- private static ReactiveCommand<Unit, Unit> CreateCommand(NavigationMetaData navigationMetaData)
+ private static Func<Task> CreateFunc(NavigationMetaData navigationMetaData)
{
- return ReactiveCommand.CreateFromTask(async () =>
+ return async () =>
{
var vm = await NavigationManager.MaterialiseViewModelAsync(navigationMetaData);
if (vm is null)
{
return;
}
+
Navigate(vm.DefaultTarget).To(vm);
- });
+ };
}
private static INavigationStack<RoutableViewModel> Navigate(NavigationTarget currentTarget)

ADD
WalletWasabi.Fluent/ViewModels/SearchBar/SearchItemViewModel.cs
+using System.Collections.Generic;
+using System.Reactive;
+using ReactiveUI;
+
+namespace WalletWasabi.Fluent.ViewModels.SearchBar;
+
+public class SearchItemViewModel
+{
+ private readonly SearchItem _item;
+
+ public SearchItemViewModel(SearchItem item, Action collapseParent)
+ {
+ _item = item;
+ Command = ReactiveCommand.CreateFromTask(async () =>
+ {
+ collapseParent();
+ await item.OnExecution();
+ });
+ }
+
+ public ReactiveCommand<Unit, Unit> Command { get; set; }
+
+ public ComposedKey Key => _item.Key;
+ public string Name => _item.Name;
+ public IEnumerable<string> Keywords => _item.Keywords;
+ public string Category => _item.Category;
+ public string Description => _item.Description;
+ public string? Icon => _item.Icon;
+}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Views/SearchBar/SearchBar.axaml
WalletWasabi.Fluent/Views/SearchBar/SearchBar.axaml
xmlns:s="clr-namespace:WalletWasabi.Fluent.Views.SearchBar"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
- xmlns:behaviors="clr-namespace:WalletWasabi.Fluent.Behaviors"
+ xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
+ xmlns:ic="clr-namespace:Avalonia.Xaml.Interactions.Custom;assembly=Avalonia.Xaml.Interactions"
xmlns:searchBar="clr-namespace:WalletWasabi.Fluent.ViewModels.SearchBar"
+ xmlns:behaviors="clr-namespace:WalletWasabi.Fluent.Behaviors"
x:CompileBindings="True"
x:DataType="searchBar:SearchBarViewModel"
x:Class="WalletWasabi.Fluent.Views.SearchBar.SearchBar">
<UserControl.Resources>
<Thickness x:Key="TextControlBorderThemeThicknessFocused">1</Thickness>
</UserControl.Resources>
+
+ <Design.DataContext>
+ <searchBar:SearchBarDesignViewModel />
+ </Design.DataContext>
+
<UserControl.Styles>
<Style Selector="TextBox#SearchBox">
</Style>
</UserControl.Styles>
- <i:Interaction.Behaviors>
- <behaviors:SearchBarBehavior SearchBox="SearchBox" SearchPanel="SearchBarDropdown" />
- </i:Interaction.Behaviors>
-
- <Design.DataContext>
- <searchBar:SearchBarDesignViewModel />
- </Design.DataContext>
-
<TextBox x:Name="SearchBox" Watermark="Search settings / advanced features" HorizontalAlignment="Left"
Text="{Binding SearchText, Mode=TwoWay}" Width="400" VerticalAlignment="Center">
<TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<PathIcon Data="{StaticResource search_regular}" Margin="10 0" />
</TextBox.InnerRightContent>
+ <i:Interaction.Behaviors>
+ <RoutedEventTriggerBehavior RoutedEvent="{x:Static InputElement.PointerPressedEvent}" RoutingStrategies="Tunnel">
+ <ia:InvokeCommandAction Command="{Binding ShowListCommand}" />
+ </RoutedEventTriggerBehavior>
+ </i:Interaction.Behaviors>
</TextBox>
<FlyoutBase.AttachedFlyout>
<Flyout Placement="Bottom" ShowMode="Transient" FlyoutPresenterClasses="search">
<s:SearchBarDropdown x:Name="SearchBarDropdown" />
</Flyout>
+
</FlyoutBase.AttachedFlyout>
+
+ <i:Interaction.Behaviors>
+ <behaviors:ShowAssociatedFlyoutBehavior IsSearchPanelOpen="{Binding IsSearchListVisible, Mode=TwoWay}"/>
+ </i:Interaction.Behaviors>
+
</UserControl>
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Views/SearchBar/SearchBarDropdown.axaml
WalletWasabi.Fluent/Views/SearchBar/SearchBarDropdown.axaml
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"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
xmlns:ic="clr-namespace:Avalonia.Xaml.Interactions.Custom;assembly=Avalonia.Xaml.Interactions"
x:Class="WalletWasabi.Fluent.Views.SearchBar.SearchBarDropdown"
- x:CompileBindings="True"
+ x:CompileBindings="False"
x:DataType="sb:SearchBarViewModel"
xmlns:conv="clr-namespace:WalletWasabi.Fluent.Converters"
xmlns:sb="clr-namespace:WalletWasabi.Fluent.ViewModels.SearchBar">
</Setter.Value>
</Setter>
</Style>
+
</UserControl.Styles>
- <ItemsControl Items="{Binding Groups}" Margin="10">
- <ItemsControl.ItemTemplate>
- <DataTemplate DataType="{x:Type sb:SearchItemGroup}">
- <DockPanel Margin="10 ">
- <TextBlock Text="{Binding Title}" DockPanel.Dock="Top" />
- <ListBox x:Name="ListBox" Items="{Binding Items}">
+ <UserControl.DataTemplates>
+ <DataTemplate x:DataType="sb:SearchItemViewModel">
+ <Button Command="{Binding Command}" Background="Transparent" HorizontalAlignment="Stretch" Padding="0">
- <ListBox.ItemTemplate>
- <DataTemplate x:DataType="sb:SearchItem">
- <Panel Background="Transparent">
<DockPanel Background="Transparent" Margin="10">
<PathIcon DockPanel.Dock="Left" Width="16" Height="16" Opacity="0.8" VerticalAlignment="Center">
<PathIcon.Data>
</PathIcon.Data>
</PathIcon>
<TextBlock Margin="10 0" VerticalAlignment="Center" DockPanel.Dock="Left" Text="{Binding Name}" />
- <TextBlock FontWeight="ExtraLight" VerticalAlignment="Center" Text="{Binding Description}" />
+ <TextBlock FontWeight="ExtraLight" VerticalAlignment="Center" Text="{Binding Description}" TextTrimming="CharacterEllipsis" />
</DockPanel>
- <i:Interaction.Behaviors>
- <ic:RoutedEventTriggerBehavior RoutedEvent="{x:Static InputElement.PointerPressedEvent}">
- <ia:InvokeCommandAction Command="{Binding Command}" />
- </ic:RoutedEventTriggerBehavior>
- </i:Interaction.Behaviors>
- </Panel>
+ </Button>
</DataTemplate>
- </ListBox.ItemTemplate>
+ </UserControl.DataTemplates>
- </ListBox>
+ <ItemsControl Items="{Binding Groups}" Margin="10">
+ <ItemsControl.ItemTemplate>
+ <DataTemplate DataType="{x:Type sb:SearchItemGroup}">
+ <DockPanel Margin="10 ">
+ <TextBlock Text="{Binding Title}" DockPanel.Dock="Top" />
+ <ItemsControl x:Name="ListBox" Items="{Binding Items}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>

Refactored flyout show/hide behavior