Implemented a few ToDo's

- Check required fields before publishing
- Allow cancelling of package downloading
- Check for JsonConverter attribute
- Improved read/unread message separator
This commit is contained in:
Mark van Renswoude 2022-01-10 11:52:07 +01:00
parent 5bc2096a24
commit 785ddbd5b2
13 changed files with 172 additions and 79 deletions

View File

@ -54,12 +54,13 @@ namespace PettingZoo.Tapeti
progressWindow.Top = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2;
progressWindow.Show();
var cancellationToken = progressWindow.CancellationToken;
Task.Run(async () =>
{
try
{
// TODO allow cancelling (by closing the progress window and optionally a Cancel button)
var assemblies = await args.Assemblies.GetAssemblies(progressWindow, CancellationToken.None);
var assemblies = await args.Assemblies.GetAssemblies(progressWindow, cancellationToken);
// var classes =
var examples = LoadExamples(assemblies);
@ -94,10 +95,11 @@ namespace PettingZoo.Tapeti
// ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead
progressWindow?.Close();
MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
if (e is not OperationCanceledException)
MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
});
}, CancellationToken.None);
});
};

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PettingZoo.Tapeti
@ -59,7 +60,20 @@ namespace PettingZoo.Tapeti
actualType = equivalentType;
// TODO check for JsonConverter attribute? doubt we'll be able to generate a nice value for it, but at least we can provide a placeholder
try
{
if (type.GetCustomAttribute<JsonConverterAttribute>() != null)
{
// This type uses custom Json conversion so there's no way to know how to provide an example.
// We could try to create an instance of the type and pass it through the converter, but for now we'll
// just output a placeholder.
return "<custom JsonConverter - manual input required>";
}
}
catch
{
// Move along
}
// String is also a class
if (actualType == typeof(string))

View File

@ -60,6 +60,15 @@ namespace PettingZoo.Tapeti.UI.PackageProgress {
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string ButtonCancel {
get {
return ResourceManager.GetString("ButtonCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reading message classes....
/// </summary>

View File

@ -112,11 +112,14 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ButtonCancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Reading message classes...</value>
</data>

View File

@ -5,10 +5,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:packageProgress="clr-namespace:PettingZoo.Tapeti.UI.PackageProgress"
mc:Ignorable="d"
Height="80"
Width="400"
Title="{x:Static packageProgress:PackageProgressStrings.WindowTitle}"
ResizeMode="NoResize"
WindowStyle="ToolWindow">
<ProgressBar Height="25" Margin="16" VerticalAlignment="Center" Name="Progress" Maximum="100" />
WindowStyle="ToolWindow"
SizeToContent="Height">
<StackPanel Orientation="Vertical">
<ProgressBar Height="25" Margin="16" VerticalAlignment="Center" Name="Progress" Maximum="100" />
<Button HorizontalAlignment="Center" Margin="0,0,0,16" Content="{x:Static packageProgress:PackageProgressStrings.ButtonCancel}" Click="CancelButton_OnClick" />
</StackPanel>
</Window>

View File

@ -1,4 +1,7 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
namespace PettingZoo.Tapeti.UI.PackageProgress
{
@ -7,9 +10,19 @@ namespace PettingZoo.Tapeti.UI.PackageProgress
/// </summary>
public partial class PackageProgressWindow : IProgress<int>
{
private readonly CancellationTokenSource cancellationTokenSource = new();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public PackageProgressWindow()
{
InitializeComponent();
Closed += (_, _) =>
{
cancellationTokenSource.Cancel();
};
}
@ -20,5 +33,11 @@ namespace PettingZoo.Tapeti.UI.PackageProgress
Progress.Value = value;
});
}
private void CancelButton_OnClick(object sender, RoutedEventArgs e)
{
cancellationTokenSource.Cancel();
((Button)sender).IsEnabled = false;
}
}
}

View File

@ -88,7 +88,6 @@ namespace PettingZoo.Tapeti.UI.PackageSelection
public ICommand AssemblyBrowse => assemblyBrowse;
// TODO hint for extra assemblies path
public static string HintNuGetSources => string.Format(PackageSelectionStrings.HintNuGetSources, PettingZooPaths.InstallationRoot, PettingZooPaths.AppDataRoot);
public string NuGetSearchTerm

View File

@ -1,12 +1,12 @@
Must-have
---------
- Check required fields before enabling Publish button
Should-have
-----------
- Save / load publisher messages (either as templates or to disk)
- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line
- Tapeti: import Tapeti.Cmd JSON file into Subscriber-esque tab for easier browsing
- Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround

View File

@ -45,7 +45,9 @@ namespace PettingZoo.UI.Tab.Publisher
public bool SendToExchange
{
get => sendToExchange;
set => SetField(ref sendToExchange, value, otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility), nameof(Title) });
set => SetField(ref sendToExchange, value,
delegateCommandsChanged: new [] { publishCommand },
otherPropertiesChanged: new[] { nameof(SendToQueue), nameof(ExchangeVisibility), nameof(QueueVisibility), nameof(Title) });
}
@ -59,21 +61,21 @@ namespace PettingZoo.UI.Tab.Publisher
public string Exchange
{
get => exchange;
set => SetField(ref exchange, value);
set => SetField(ref exchange, value, delegateCommandsChanged: new[] { publishCommand });
}
public string RoutingKey
{
get => routingKey;
set => SetField(ref routingKey, value, otherPropertiesChanged: new[] { nameof(Title) });
set => SetField(ref routingKey, value, delegateCommandsChanged: new[] { publishCommand }, otherPropertiesChanged: new[] { nameof(Title) });
}
public string Queue
{
get => queue;
set => SetField(ref queue, value, otherPropertiesChanged: new[] { nameof(Title) });
set => SetField(ref queue, value, delegateCommandsChanged: new[] { publishCommand }, otherPropertiesChanged: new[] { nameof(Title) });
}
@ -183,6 +185,17 @@ namespace PettingZoo.UI.Tab.Publisher
private bool PublishCanExecute()
{
if (SendToExchange)
{
if (string.IsNullOrWhiteSpace(Exchange) || string.IsNullOrWhiteSpace(RoutingKey))
return false;
}
else
{
if (string.IsNullOrWhiteSpace(Queue))
return false;
}
return messageTypePublishCommand?.CanExecute(null) ?? false;
}
@ -197,6 +210,11 @@ namespace PettingZoo.UI.Tab.Publisher
if (rawPublisherView == null)
{
rawPublisherViewModel = new RawPublisherViewModel(connection, this);
rawPublisherViewModel.PublishCommand.CanExecuteChanged += (_, _) =>
{
publishCommand.RaiseCanExecuteChanged();
};
rawPublisherView ??= new RawPublisherView(rawPublisherViewModel);
}
else
@ -213,6 +231,11 @@ namespace PettingZoo.UI.Tab.Publisher
if (tapetiPublisherView == null)
{
tapetiPublisherViewModel = new TapetiPublisherViewModel(connection, this, exampleGenerator);
tapetiPublisherViewModel.PublishCommand.CanExecuteChanged += (_, _) =>
{
publishCommand.RaiseCanExecuteChanged();
};
tapetiPublisherView ??= new TapetiPublisherView(tapetiPublisherViewModel);
if (tabHostWindow != null)

View File

@ -115,7 +115,7 @@ namespace PettingZoo.UI.Tab.Publisher
public string Payload
{
get => payload;
set => SetField(ref payload, value);
set => SetField(ref payload, value, delegateCommandsChanged: new [] { publishCommand });
}
@ -267,10 +267,9 @@ namespace PettingZoo.UI.Tab.Publisher
}
private static bool PublishCanExecute()
private bool PublishCanExecute()
{
// TODO validate input
return true;
return !string.IsNullOrWhiteSpace(Payload);
}

View File

@ -36,10 +36,15 @@ namespace PettingZoo.UI.Tab.Publisher
public string ClassName
{
get => string.IsNullOrEmpty(className) ? AssemblyName + "." : className;
get => string.IsNullOrWhiteSpace(className)
? string.IsNullOrWhiteSpace(AssemblyName)
? ""
: AssemblyName + "."
: className;
set
{
if (SetField(ref className, value))
if (SetField(ref className, value, delegateCommandsChanged: new[] { publishCommand }))
validatingExample = null;
}
}
@ -48,7 +53,7 @@ namespace PettingZoo.UI.Tab.Publisher
public string AssemblyName
{
get => assemblyName;
set => SetField(ref assemblyName, value, otherPropertiesChanged:
set => SetField(ref assemblyName, value, delegateCommandsChanged: new[] { publishCommand }, otherPropertiesChanged:
string.IsNullOrEmpty(value) || string.IsNullOrEmpty(className)
? new [] { nameof(ClassName) }
: null
@ -59,7 +64,7 @@ namespace PettingZoo.UI.Tab.Publisher
public string Payload
{
get => payload;
set => SetField(ref payload, value);
set => SetField(ref payload, value, delegateCommandsChanged: new[] { publishCommand });
}
@ -164,12 +169,15 @@ namespace PettingZoo.UI.Tab.Publisher
}
private static bool PublishCanExecute()
private bool PublishCanExecute()
{
// TODO validate input
return true;
return
!string.IsNullOrWhiteSpace(assemblyName) &&
!string.IsNullOrWhiteSpace(ClassName) &&
!string.IsNullOrWhiteSpace(Payload);
}
public void HostWindowChanged(Window? hostWindow)
{
tabHostWindow = hostWindow;

View File

@ -7,6 +7,7 @@
xmlns:res="clr-namespace:PettingZoo.UI.Tab.Subscriber"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:connection="clr-namespace:PettingZoo.Core.Connection;assembly=PettingZoo.Core"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
@ -24,48 +25,46 @@
<ListBox Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding Messages}"
SelectedItem="{Binding Path=SelectedMessage, Mode=TwoWay}"
ui:ListBox.AutoScroll="True"
x:Name="ReferenceControlForBorder">
x:Name="ReferenceControlForBorder"
Grid.IsSharedSizeScope="True">
<ListBox.Resources>
<ui:BindingProxy x:Key="ContextMenuProxy" Data="{Binding}" />
<CollectionViewSource x:Key="Messages"
Source="{Binding Messages}" />
<CollectionViewSource x:Key="UnreadMessages"
Source="{Binding UnreadMessages}" />
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="150" SharedSizeGroup="DateTime"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- TODO insert as non-focusable item instead, so it's not part of the selection (and perhaps also fixes the bug mentioned in SubscriberViewModel) -->
<Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2">
<ListBox.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource Messages}}" />
<ListBoxItem HorizontalContentAlignment="Stretch" IsEnabled="False" IsHitTestVisible="False">
<Grid Visibility="{Binding UnreadMessagesVisibility}">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="DateTime" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Visibility>
<MultiBinding Converter="{StaticResource SameMessageVisibilityConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="DataContext" />
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ListBox}}" Path="DataContext.NewMessage" />
</MultiBinding>
</Grid.Visibility>
<Separator Grid.Column="0" Margin="0,0,8,0" />
<TextBlock Grid.Column="1" Text="{x:Static res:SubscriberViewStrings.LabelNewMessages}" HorizontalAlignment="Center" Background="{Binding Background, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Foreground="{x:Static SystemColors.GrayTextBrush}" />
<Separator Grid.Column="2" Margin="8,0,0,0" />
</Grid>
</Grid>
</ListBoxItem>
<CollectionContainer Collection="{Binding Source={StaticResource UnreadMessages}}" />
</CompositeCollection>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type connection:ReceivedMessageInfo}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="150" SharedSizeGroup="DateTime"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="1" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}" />
<TextBlock Grid.Column="0" Text="{Binding ReceivedTimestamp, StringFormat=g}" Style="{StaticResource Timestamp}" />
<TextBlock Grid.Column="1" Text="{Binding RoutingKey}" Style="{StaticResource RoutingKey}" />
<Grid.ContextMenu>
<ContextMenu>

View File

@ -1,17 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Rendering;
using PettingZoo.WPF.ViewModel;
// TODO if the "New message" line is visible when this tab is undocked, the line in the ListBox does not shrink. Haven't been able to figure out yet how to solve it
namespace PettingZoo.UI.Tab.Subscriber
{
public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands, ITabActivate
@ -29,7 +27,6 @@ namespace PettingZoo.UI.Tab.Subscriber
private readonly DelegateCommand createPublisherCommand;
private bool tabActive;
private ReceivedMessageInfo? newMessage;
private Timer? newMessageTimer;
private int unreadCount;
@ -39,7 +36,8 @@ namespace PettingZoo.UI.Tab.Subscriber
// ReSharper disable once UnusedMember.Global - it is, but via a proxy
public ICommand CreatePublisherCommand => createPublisherCommand;
public ObservableCollection<ReceivedMessageInfo> Messages { get; }
public ObservableCollectionEx<ReceivedMessageInfo> Messages { get; }
public ObservableCollectionEx<ReceivedMessageInfo> UnreadMessages { get; }
public ReceivedMessageInfo? SelectedMessage
{
@ -52,11 +50,7 @@ namespace PettingZoo.UI.Tab.Subscriber
}
public ReceivedMessageInfo? NewMessage
{
get => newMessage;
set => SetField(ref newMessage, value);
}
public Visibility UnreadMessagesVisibility => UnreadMessages.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
public string SelectedMessageBody =>
@ -85,7 +79,8 @@ namespace PettingZoo.UI.Tab.Subscriber
dispatcher = Dispatcher.CurrentDispatcher;
Messages = new ObservableCollection<ReceivedMessageInfo>();
Messages = new ObservableCollectionEx<ReceivedMessageInfo>();
UnreadMessages = new ObservableCollectionEx<ReceivedMessageInfo>();
clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
toolbarCommands = new[]
@ -103,6 +98,8 @@ namespace PettingZoo.UI.Tab.Subscriber
private void ClearExecute()
{
Messages.Clear();
UnreadMessages.Clear();
RaisePropertyChanged(nameof(UnreadMessagesVisibility));
clearCommand.RaiseCanExecuteChanged();
}
@ -135,10 +132,13 @@ namespace PettingZoo.UI.Tab.Subscriber
unreadCount++;
RaisePropertyChanged(nameof(Title));
NewMessage ??= args.MessageInfo;
UnreadMessages.Add(args.MessageInfo);
if (UnreadMessages.Count == 1)
RaisePropertyChanged(nameof(UnreadMessagesVisibility));
}
else
Messages.Add(args.MessageInfo);
Messages.Add(args.MessageInfo);
clearCommand.RaiseCanExecuteChanged();
});
}
@ -161,7 +161,7 @@ namespace PettingZoo.UI.Tab.Subscriber
RaisePropertyChanged(nameof(Title));
if (NewMessage == null)
if (UnreadMessages.Count == 0)
return;
newMessageTimer?.Dispose();
@ -170,7 +170,23 @@ namespace PettingZoo.UI.Tab.Subscriber
{
dispatcher.BeginInvoke(() =>
{
NewMessage = null;
if (UnreadMessages.Count == 0)
return;
Messages.BeginUpdate();
UnreadMessages.BeginUpdate();
try
{
Messages.AddRange(UnreadMessages);
UnreadMessages.Clear();
}
finally
{
UnreadMessages.EndUpdate();
Messages.EndUpdate();
}
RaisePropertyChanged(nameof(UnreadMessagesVisibility));
});
},
null,
@ -178,6 +194,7 @@ namespace PettingZoo.UI.Tab.Subscriber
Timeout.InfiniteTimeSpan);
}
public void Deactivate()
{
if (newMessageTimer != null)
@ -186,7 +203,6 @@ namespace PettingZoo.UI.Tab.Subscriber
newMessageTimer = null;
}
NewMessage = null;
tabActive = false;
}
}
@ -197,22 +213,21 @@ namespace PettingZoo.UI.Tab.Subscriber
public DesignTimeSubscriberViewModel() : base(null!, null!, null!, new DesignTimeSubscriber())
{
for (var i = 1; i <= 5; i++)
Messages.Add(new ReceivedMessageInfo(
"designtime",
$"designtime.message.{i}",
Encoding.UTF8.GetBytes(@"Design-time message"),
(i > 2 ? UnreadMessages : Messages).Add(new ReceivedMessageInfo(
"designtime",
$"designtime.message.{i}",
Encoding.UTF8.GetBytes(@"Design-time message"),
new MessageProperties(null)
{
ContentType = "text/fake",
ReplyTo = "/dev/null"
},
},
DateTime.Now));
SelectedMessage = Messages[2];
NewMessage = Messages[2];
SelectedMessage = UnreadMessages[0];
}
private class DesignTimeSubscriber : ISubscriber
{
public ValueTask DisposeAsync()