Session: redesign result cards, saved queries always visible, collapsible sections

- Move all action buttons (star, expand, JSON, trash) into each result card header
- Always show query text in result cards (no conditional hiding)
- Saved queries section permanently visible above results, collapsible via chevron
- Result cards collapsible via per-card chevron toggle
- Add drop shadow and remove border around query text
- Clear query field after execute; tap result query label to reload it into editor
- WebView re-renders HTML on App.ThemeChanged for instant dark/light switch
- Result card trash, delete result, save/load query, open graph/table all work per-card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gerwin Kuijntjes
2026-03-31 14:47:48 +02:00
parent a390a07d62
commit 249185a146
6 changed files with 414 additions and 140 deletions

View File

@@ -2,28 +2,28 @@
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
Margin="0, 0, 0, 25"
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
<StackLayout Orientation="Vertical" Spacing="0">
<Border Stroke="{StaticResource QueryTextBorder}" StrokeThickness="4">
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
x:Class="Xamarin.Neo4j.Controls.QueryResultView"
x:Name="self">
<Grid VerticalOptions="Start">
<!-- Graph WebView -->
<WebView x:Name="graphView"
HorizontalOptions="FillAndExpand"
IsVisible="{Binding CanDisplayGraph}"
HeightRequest="{Binding GraphViewHeight, Source={x:Reference self}}" />
<!-- Error display -->
<Border IsVisible="{Binding IsError}"
BackgroundColor="#ffebee"
StrokeThickness="0"
Padding="12">
<StackLayout Spacing="6">
<Label Text="{Binding ErrorMessage}"
TextColor="#c62828"
FontSize="14"
FontFamily="RobotoMono" />
</StackLayout>
</Border>
<WebView x:Name="graphView" HorizontalOptions="FillAndExpand" IsVisible="{Binding CanDisplayGraph}" HeightRequest="200" />
<Grid BackgroundColor="{StaticResource QueryActionBar}" Padding="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Clicked="SaveQuery" Text="{x:Static fonts:FontAwesomeSolid.Star}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20" />
<Button Grid.Column="1" Clicked="OpenNeovis" Text="{x:Static fonts:FontAwesomeSolid.Expand}" BackgroundColor="Transparent" IsEnabled="{Binding CanDisplayGraph}" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
<Button Grid.Column="2" Clicked="OpenTableView" Text="{x:Static fonts:FontAwesomeSolid.List}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
<Button Grid.Column="3" Clicked="CloseResultView" Text="{x:Static fonts:FontAwesomeSolid.Times}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
</Grid>
</StackLayout>
</Grid>
</ContentView>

View File

@@ -1,43 +1,49 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;
using Neo4j.Driver;
using Xamarin.Neo4j.Managers;
using Xamarin.Neo4j.Services;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.Pages;
using Xamarin.Neo4j.Utilities;
using Query = Xamarin.Neo4j.Models.Query;
namespace Xamarin.Neo4j.Controls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class QueryResultView : ContentView
{
private QueryResult QueryResult => (QueryResult) BindingContext;
public static readonly BindableProperty GraphViewHeightProperty =
BindableProperty.Create(nameof(GraphViewHeight), typeof(double), typeof(QueryResultView), 300.0);
public event EventHandler<GenericEventArgs<QueryResult>> CloseRequested;
public double GraphViewHeight
{
get => (double)GetValue(GraphViewHeightProperty);
set => SetValue(GraphViewHeightProperty, value);
}
private string _neovisHtml;
private QueryResult QueryResult => (QueryResult)BindingContext;
public QueryResultView()
{
InitializeComponent();
BindingContextChanged += OnBindingContextChanged;
graphView.Navigating += OnGraphViewNavigating;
App.ThemeChanged += OnThemeChanged;
}
private void OnThemeChanged(object sender, EventArgs e)
{
if (QueryResult == null || graphView == null) return;
ParseNeovisHtmlSafe();
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
}
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
{
Console.WriteLine($"[Graph] Inline Navigating: {e.Url}");
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
e.Cancel = true;
@@ -69,19 +75,11 @@ namespace Xamarin.Neo4j.Controls
private void OnBindingContextChanged(object sender, EventArgs e)
{
if (BindingContext == null || graphView == null)
{
Console.WriteLine($"[Graph] OnBindingContextChanged skipped: BindingContext={BindingContext}, graphView={graphView}");
return;
}
Console.WriteLine($"[Graph] OnBindingContextChanged fired, CanDisplayGraph={QueryResult?.CanDisplayGraph}");
if (BindingContext == null || graphView == null) return;
ParseNeovisHtmlSafe();
Console.WriteLine($"[Graph] Setting graphView.Source, html length={_neovisHtml?.Length ?? 0}");
graphView.Source = new HtmlWebViewSource { Html = _neovisHtml };
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
}
private void ParseNeovisHtml()
@@ -89,83 +87,46 @@ namespace Xamarin.Neo4j.Controls
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
var available = string.Join("\n", assembly.GetManifestResourceNames());
Console.WriteLine($"[Graph] Looking for: {resourceName}");
Console.WriteLine($"[Graph] Available resources: {available.Replace("\n", ", ")}");
var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
$"<b>Available:</b><br>{available.Replace("\n", "<br>")}</body></html>";
var available = string.Join(", ", assembly.GetManifestResourceNames());
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
$"<b>Available:</b><br>{available}</body></html>";
return;
}
using (stream)
using (var reader = new StreamReader(stream))
{
var result = reader.ReadToEnd();
var html = reader.ReadToEnd();
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
result = result.Replace("{{nodes}}", nodesJson);
result = result.Replace("{{edges}}", edgesJson);
result = result.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
result = result.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
html = html.Replace("{{nodes}}", nodesJson);
html = html.Replace("{{edges}}", edgesJson);
html = html.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
html = html.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
_neovisHtml = result;
QueryResult.NeovisHtml = html;
}
}
private void ParseNeovisHtmlSafe()
{
if (QueryResult == null) return;
try
{
ParseNeovisHtml();
}
catch (Exception ex)
{
Console.WriteLine($"[Graph] ParseNeovisHtml threw: {ex.GetType().Name}: {ex.Message}");
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
$"<b>{ex.GetType().Name}</b><br>{ex.Message}<br><br>{ex.StackTrace?.Replace("\n", "<br>")}</body></html>";
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
$"<b>{ex.GetType().Name}</b><br>{ex.Message}</body></html>";
}
}
private async void OpenNeovis(object sender, EventArgs e)
{
var neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
var connectionString = ConnectionStringManager.ActiveConnectionString;
await Application.Current.MainPage.Navigation.PushAsync(new GraphPage(_neovisHtml, connectionString, neo4jService));
}
private async void SaveQuery(object sender, EventArgs e)
{
var name = await Application.Current.MainPage.DisplayPromptAsync("Save Query", "How should the query be called?");
if (!string.IsNullOrWhiteSpace(name))
{
var query = new Query()
{
Id = Guid.NewGuid(),
QueryText = QueryResult.Query,
Name = name,
};
SavedQueryManager.AddSavedQuery(query);
}
}
private void CloseResultView(object sender, EventArgs e)
{
CloseRequested?.Invoke(sender, new GenericEventArgs<QueryResult>(QueryResult));
}
private async void OpenTableView(object sender, EventArgs e)
{
await Application.Current.MainPage.Navigation.PushAsync(new TablePage(QueryResult));
}
}
}

View File

@@ -31,5 +31,10 @@ namespace Xamarin.Neo4j.Models
public Neo4jConnectionString ConnectionString { get; set; }
public Dictionary<string, List<object>> Results { get; set; }
/// <summary>Cached NeoVis HTML generated by QueryResultView; set when the result is first rendered.</summary>
public string NeovisHtml { get; set; }
public bool IsError => !Success;
}
}

View File

@@ -4,7 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
x:Class="Xamarin.Neo4j.Pages.SessionPage">
x:Class="Xamarin.Neo4j.Pages.SessionPage"
x:Name="sessionPage">
<NavigationPage.TitleView>
<StackLayout Spacing="0">
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
@@ -35,40 +36,202 @@
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand">
<Border BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
<Grid RowDefinitions="Auto,*">
<!-- Row 0: Query editor -->
<Border Grid.Row="0" BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
<Border Stroke="#d8e5f1" StrokeThickness="2" StrokeShape="RoundRectangle 2">
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
</Border>
</Border>
<Grid VerticalOptions="FillAndExpand">
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
<CollectionView.ItemTemplate>
<DataTemplate>
<controls:QueryResultView CloseRequested="CloseResultView" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Row 1: Single scroll area — results then saved queries always visible -->
<ScrollView Grid.Row="1" x:Name="mainScroll">
<StackLayout Spacing="0" Padding="0,0,0,16">
<!-- Saved queries section — collapsible, always above results -->
<Grid ColumnDefinitions="*,Auto" Padding="16,10,4,4">
<Label Grid.Column="0"
Text="Saved Queries"
FontSize="12" FontAttributes="Bold"
TextColor="{DynamicResource SecondaryTextColor}"
VerticalOptions="Center" />
<Button Grid.Column="1"
x:Name="savedQueriesChevron"
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="11"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
WidthRequest="40" HeightRequest="32"
Clicked="ToggleSavedQueries" />
</Grid>
<StackLayout x:Name="savedQueriesContent" Spacing="0" Margin="16,0">
<StackLayout BindableLayout.ItemsSource="{Binding SavedQueries}"
IsVisible="{Binding HasSavedQueries}"
Spacing="0">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,Auto" Padding="0,10">
<StackLayout Grid.Column="0" Spacing="2">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.Commands[LoadQuery], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
</StackLayout.GestureRecognizers>
<Label Text="{Binding Name}" FontSize="14" FontAttributes="Bold" />
<Label Text="{Binding QueryText}"
FontSize="12"
TextColor="{DynamicResource SecondaryTextColor}"
MaxLines="1"
LineBreakMode="TailTruncation" />
</StackLayout>
<Button Grid.Column="1"
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
FontFamily="{StaticResource FontAwesomeSolid}"
FontSize="16"
WidthRequest="44"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
Command="{Binding BindingContext.Commands[DeleteQuery], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
<Label Text="No saved queries yet"
IsVisible="{Binding HasNoSavedQueries}"
TextColor="{DynamicResource SecondaryTextColor}"
FontSize="14"
Margin="0,4,0,8" />
</StackLayout>
<!-- Divider between saved queries and results -->
<BoxView HeightRequest="1"
BackgroundColor="{DynamicResource SecondaryTextColor}"
Opacity="0.15"
Margin="16,8,16,0" />
<!-- Results section -->
<StackLayout IsVisible="{Binding HasResults}" Spacing="0">
<Grid ColumnDefinitions="*,Auto" Padding="16,8,16,4">
<Label Grid.Column="0"
Text="Results"
FontSize="12" FontAttributes="Bold"
TextColor="{DynamicResource SecondaryTextColor}"
VerticalOptions="Center" />
<Button Grid.Column="1"
Text="Clear all"
FontSize="11"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
Padding="8,0"
HeightRequest="32"
Command="{Binding Commands[ClearResults]}" />
</Grid>
<StackLayout BindableLayout.ItemsSource="{Binding QueryResults}" Spacing="0">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Border Margin="12,0,12,8"
BackgroundColor="{DynamicResource Extreme}"
StrokeShape="RoundRectangle 8"
StrokeThickness="0">
<Border.Shadow>
<Shadow Brush="Black" Offset="0,2" Radius="6" Opacity="0.12" />
</Border.Shadow>
<StackLayout Spacing="0">
<!-- Action row -->
<Grid ColumnDefinitions="*,*,*,*,Auto" Padding="4,2">
<Button Grid.Column="0"
Text="{x:Static fonts:FontAwesomeSolid.Star}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
HeightRequest="40"
Command="{Binding BindingContext.Commands[SaveQuery], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
<Button Grid.Column="1"
Text="{x:Static fonts:FontAwesomeSolid.Expand}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
BackgroundColor="Transparent"
IsEnabled="{Binding CanDisplayGraph}"
TextColor="{DynamicResource SecondaryTextColor}"
HeightRequest="40"
Command="{Binding BindingContext.Commands[OpenGraph], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
<Button Grid.Column="2"
Text="{x:Static fonts:FontAwesomeSolid.Code}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
BackgroundColor="Transparent"
IsEnabled="{Binding Success}"
TextColor="{DynamicResource SecondaryTextColor}"
HeightRequest="40"
Command="{Binding BindingContext.Commands[OpenTable], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
<Button Grid.Column="3"
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
HeightRequest="40"
Command="{Binding BindingContext.Commands[DeleteResult], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
<Button Grid.Column="4"
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="12"
BackgroundColor="Transparent"
TextColor="{DynamicResource SecondaryTextColor}"
WidthRequest="40" HeightRequest="40"
Clicked="ToggleResultCollapse" />
</Grid>
<!-- Query label -->
<Label Text="{Binding DisplayQuery}"
Padding="12,0,12,8"
FontSize="12"
TextColor="{DynamicResource SecondaryTextColor}"
LineBreakMode="TailTruncation"
MaxLines="2">
<Label.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding BindingContext.Commands[LoadResultQuery], Source={x:Reference sessionPage}}"
CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
<!-- Result content -->
<controls:QueryResultView
GraphViewHeight="{Binding BindingContext.GraphViewHeight, Source={x:Reference sessionPage}}" />
</StackLayout>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</StackLayout>
<!-- Empty hint (no results yet) -->
<StackLayout IsVisible="{Binding IsEmpty}" HorizontalOptions="Center" Spacing="12" Padding="32,24">
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
FontFamily="{StaticResource FontAwesomeSolid}"
FontSize="48"
TextColor="{DynamicResource SecondaryTextColor}"
HorizontalOptions="Center" />
<Label Text="No results yet"
FontSize="18" FontAttributes="Bold"
TextColor="{DynamicResource SecondaryTextColor}"
HorizontalOptions="Center" />
<Label Text="Write a query and tap play, or load a saved query above"
FontSize="14"
TextColor="{DynamicResource SecondaryTextColor}"
HorizontalTextAlignment="Center"
HorizontalOptions="Center" />
</StackLayout>
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12" Padding="32">
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
FontFamily="{StaticResource FontAwesomeSolid}"
FontSize="48"
TextColor="{StaticResource SecondaryTextColor}"
HorizontalOptions="Center" />
<Label Text="No results yet"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource SecondaryTextColor}"
HorizontalOptions="Center" />
<Label Text="Write a query and tap the play button to run it"
FontSize="14"
TextColor="{StaticResource SecondaryTextColor}"
HorizontalTextAlignment="Center"
HorizontalOptions="Center" />
</StackLayout>
</Grid>
</StackLayout>
</ScrollView>
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@@ -1,8 +1,8 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using Xamarin.Neo4j.Fonts;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.Utilities;
using Xamarin.Neo4j.ViewModels;
namespace Xamarin.Neo4j.Pages
@@ -10,7 +10,7 @@ namespace Xamarin.Neo4j.Pages
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SessionPage : ContentPage
{
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
private SessionViewModel ViewModel => (SessionViewModel)BindingContext;
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
{
@@ -18,25 +18,58 @@ namespace Xamarin.Neo4j.Pages
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
ViewModel.ScrollToTop += (_, _) =>
ViewModel.ScrollToTop += async (_, _) =>
{
resultsCollection.ScrollTo(0, 0, ScrollToPosition.Start, true);
await mainScroll.ScrollToAsync(0, 0, true);
};
}
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.LoadSavedQueries();
}
private void FocusDatabasePicker(object sender, EventArgs e)
{
databasePicker.Focus();
}
private void CloseResultView(object sender, GenericEventArgs<QueryResult> e)
{
ViewModel.DeleteQueryResult(e.Data);
}
private void ExecuteQuery(object sender, EventArgs e)
{
ViewModel.Commands["ExecuteQuery"].Execute(null);
}
private bool _savedQueriesExpanded = true;
private void ToggleSavedQueries(object sender, EventArgs e)
{
_savedQueriesExpanded = !_savedQueriesExpanded;
savedQueriesContent.IsVisible = _savedQueriesExpanded;
savedQueriesChevron.Text = _savedQueriesExpanded
? FontAwesomeSolid.ChevronUp
: FontAwesomeSolid.ChevronDown;
}
private void ToggleResultCollapse(object sender, EventArgs e)
{
if (sender is not Button btn) return;
// Walk up to find the StackLayout that wraps action row + query + content
var card = btn.Parent?.Parent as StackLayout; // btn -> Grid -> StackLayout
if (card == null) return;
// The collapsible content is the last child (QueryResultView)
var content = card.Children[card.Children.Count - 1] as View;
// The query label is second-to-last
var queryLabel = card.Children.Count >= 2 ? card.Children[card.Children.Count - 2] as View : null;
if (content == null) return;
var collapse = content.IsVisible;
content.IsVisible = !collapse;
if (queryLabel != null) queryLabel.IsVisible = !collapse;
btn.Text = collapse ? FontAwesomeSolid.ChevronDown : FontAwesomeSolid.ChevronUp;
}
}
}

View File

@@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Xamarin.Neo4j.Annotations;
using Xamarin.Neo4j.Managers;
using Xamarin.Neo4j.Models;
using Xamarin.Neo4j.Pages;
using Xamarin.Neo4j.Services;
@@ -41,6 +42,8 @@ namespace Xamarin.Neo4j.ViewModels
private ObservableCollection<QueryResult> _queryResults;
private List<Query> _savedQueries;
private Neo4jConnectionString _connectionString;
private string _query;
@@ -54,7 +57,11 @@ namespace Xamarin.Neo4j.ViewModels
Query = initialQuery;
QueryResults = new ObservableCollection<QueryResult>();
QueryResults.CollectionChanged += (_, _) => OnPropertyChanged(nameof(IsEmpty));
QueryResults.CollectionChanged += (_, _) =>
{
OnPropertyChanged(nameof(IsEmpty));
OnPropertyChanged(nameof(HasResults));
};
Commands.Add("ExecuteQuery", new Command(async () =>
{
@@ -65,20 +72,91 @@ namespace Xamarin.Neo4j.ViewModels
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
if (result.Success)
{
QueryResults.Insert(0, result);
ScrollToTop?.Invoke(this, EventArgs.Empty);
}
else
await Application.Current.MainPage.DisplayAlert("", result.ErrorMessage, "OK");
Query = null;
QueryResults.Insert(0, result);
ScrollToTop?.Invoke(this, EventArgs.Empty);
}));
Commands.Add("DeleteQuery", new Command(async (o) =>
{
if (!(o is Query query))
return;
var confirmed = await Application.Current.MainPage.DisplayAlert(
"Delete Query",
$"Delete \"{query.Name}\"?",
"Delete", "Cancel");
if (confirmed)
{
SavedQueryManager.DeleteSavedQuery(query);
LoadSavedQueries();
}
}));
Commands.Add("LoadQuery", new Command((o) =>
{
if (o is Query query)
LoadQuery(query);
}));
Commands.Add("LoadResultQuery", new Command((o) =>
{
if (o is QueryResult result)
Query = result.Query;
}));
Commands.Add("DeleteResult", new Command((o) =>
{
if (o is QueryResult result)
DeleteQueryResult(result);
}));
Commands.Add("SaveQuery", new Command(async (o) =>
{
if (!(o is QueryResult result)) return;
var name = await Application.Current.MainPage.DisplayPromptAsync(
"Save Query", "What would you like to call this query?");
if (!string.IsNullOrWhiteSpace(name))
{
SavedQueryManager.AddSavedQuery(new Query
{
Id = Guid.NewGuid(),
QueryText = result.Query,
Name = name
});
LoadSavedQueries();
}
}));
Commands.Add("OpenGraph", new Command(async (o) =>
{
if (!(o is QueryResult result) || !result.CanDisplayGraph || result.NeovisHtml == null) return;
var connectionString2 = ConnectionStringManager.ActiveConnectionString;
await Navigation.PushAsync(new GraphPage(result.NeovisHtml, connectionString2, _neo4jService));
}));
Commands.Add("OpenTable", new Command(async (o) =>
{
if (!(o is QueryResult result) || !result.Success) return;
await Navigation.PushAsync(new TablePage(result));
}));
Commands.Add("ClearResults", new Command(() => ClearAllResults()));
InitializeConnection(connectionString);
}
public void LoadSavedQueries()
{
SavedQueries = SavedQueryManager.GetSavedQueries();
}
public void LoadQuery(Query query)
{
Query = query.QueryText;
}
private async void InitializeConnection(Neo4jConnectionString connectionString)
{
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
@@ -106,6 +184,11 @@ namespace Xamarin.Neo4j.ViewModels
QueryResults.Remove(item);
}
public void ClearAllResults()
{
QueryResults.Clear();
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
@@ -164,10 +247,39 @@ namespace Xamarin.Neo4j.ViewModels
}
}
public List<Query> SavedQueries
{
get => _savedQueries;
set
{
_savedQueries = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasSavedQueries));
OnPropertyChanged(nameof(HasNoSavedQueries));
}
}
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
public bool IsEmpty => QueryResults?.Count == 0;
public bool HasResults => !IsEmpty;
public bool HasSavedQueries => _savedQueries?.Count > 0;
public bool HasNoSavedQueries => !HasSavedQueries;
public double GraphViewHeight
{
get
{
var screenHeight = _screenSizeService.GetScreenHeight();
return Math.Max(200, screenHeight - 300);
}
}
#endregion
}
}