From bc8f5e4d54445edeb33c192023f5a4cbc6013562 Mon Sep 17 00:00:00 2001 From: lvas463 Date: Tue, 14 Nov 2023 12:50:07 -0500 Subject: [PATCH 01/21] idek tbh --- PcgsInvUi/Services/PcgsClient.cs | 45 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/PcgsInvUi/Services/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index 3b39b49..b913f64 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -9,7 +9,11 @@ using ReactiveUI; namespace PcgsInvUi.Services; +/// +/// Provides an interface into the PCGS Public API. +/// public class PcgsClient : ReactiveObject { + public int NumApiRequests { get => _numApiRequests; set => this.RaiseAndSetIfChanged(ref _numApiRequests, value); @@ -20,7 +24,7 @@ public class PcgsClient : ReactiveObject { private int _numApiRequests; public PcgsClient(string apiKey) { - // _apiKey = apiKey; + // I'd like to be able to test the API key upon initialization. _apiKey = "E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm"; } @@ -38,12 +42,15 @@ public class PcgsClient : ReactiveObject { Console.WriteLine("Got response."); if (response.IsSuccessStatusCode) { + // TODO Check for errors indicated by a successful response. string jsonResponse = await response.Content.ReadAsStringAsync(); newCoin = ParseJsonResponse(jsonResponse); Console.WriteLine("Coin created."); } else { - Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}"); + // TODO Throw HTTP exception. + Console.WriteLine("Error getting coin."); + } } @@ -68,4 +75,36 @@ public class PcgsClient : ReactiveObject { return newCoin; } -} \ No newline at end of file +} + +/// +/// Thrown when the API response indicates that the request was invalid. +/// Generally used when the request was formatted incorrectly. +/// +public class InvalidRequestException : Exception { + public InvalidRequestException() { } + public InvalidRequestException(string message) : base(message) { } + public InvalidRequestException(string message, Exception inner) : base(message, inner) { } +} + +/// +/// Thrown when the API response indicates that the coin was not found. +/// Generally used when the PCGS number and grade combo that was requested does not exist in the PCGS database. +/// +public class CoinNotFoundException : Exception { + public CoinNotFoundException() { } + public CoinNotFoundException(string message) : base(message) { } + public CoinNotFoundException(string message, Exception inner) : base(message, inner) { } +} + +/// +/// Thrown when the API returns an HTTP error 401. +/// Generally indicates that the API key is invalid. +/// +public class ApiAuthorizationException : Exception { + public ApiAuthorizationException() { } + public ApiAuthorizationException(string message) : base(message) { } + public ApiAuthorizationException(string message, Exception inner) : base(message, inner) { } +} + +public -- 2.54.0 From d667e86b1b73b6acff065a6698c5129a639f50dd Mon Sep 17 00:00:00 2001 From: lvas463 Date: Tue, 14 Nov 2023 19:07:11 +0000 Subject: [PATCH 02/21] Added most of the framework for the error window. --- PcgsInvUi/Services/PcgsClient.cs | 61 ++++++--------------- PcgsInvUi/ViewModels/MainWindowViewModel.cs | 14 ++++- PcgsInvUi/Views/MainWindow.axaml.cs | 7 +++ 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/PcgsInvUi/Services/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index b913f64..3e2af44 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -1,19 +1,16 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using PcgsInvUi.Models; using System.Text.Json; +using DynamicData.Kernel; using ReactiveUI; - namespace PcgsInvUi.Services; -/// -/// Provides an interface into the PCGS Public API. -/// public class PcgsClient : ReactiveObject { - public int NumApiRequests { get => _numApiRequests; set => this.RaiseAndSetIfChanged(ref _numApiRequests, value); @@ -24,12 +21,12 @@ public class PcgsClient : ReactiveObject { private int _numApiRequests; public PcgsClient(string apiKey) { - // I'd like to be able to test the API key upon initialization. + // _apiKey = apiKey; _apiKey = "E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm"; } - public async Task GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { + public async Task GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { Coin newCoin = null; using (HttpClient client = new HttpClient()) { var gradeParam = GetGradeFromString(grade); @@ -42,15 +39,12 @@ public class PcgsClient : ReactiveObject { Console.WriteLine("Got response."); if (response.IsSuccessStatusCode) { - // TODO Check for errors indicated by a successful response. string jsonResponse = await response.Content.ReadAsStringAsync(); newCoin = ParseJsonResponse(jsonResponse); Console.WriteLine("Coin created."); } else { - // TODO Throw HTTP exception. - Console.WriteLine("Error getting coin."); - + Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}"); } } @@ -65,46 +59,27 @@ public class PcgsClient : ReactiveObject { // TODO Will eventually create the other methods that are included in the Public API. - private Coin ParseJsonResponse(string json) { + private Coin? ParseJsonResponse(string json) { Console.WriteLine(json); - Coin newCoin = JsonSerializer.Deserialize(json); - Console.WriteLine($"New Coin has pcgs number {newCoin.PCGSNo}"); - - string test = JsonSerializer.Serialize(newCoin); - Console.WriteLine(test); + // TODO Check for errors in the response + if (json.Contains("\"ServerMessage\":\"No data found\"")) throw new CoinNotFoundException("No coin found with that PCGS number."); + if (json.Contains("\"IsValidRequest\":true")) throw new InvalidRequestFormatException("Invalid request format."); + var newCoin = JsonSerializer.Deserialize(json); return newCoin; } } -/// -/// Thrown when the API response indicates that the request was invalid. -/// Generally used when the request was formatted incorrectly. -/// -public class InvalidRequestException : Exception { - public InvalidRequestException() { } - public InvalidRequestException(string message) : base(message) { } - public InvalidRequestException(string message, Exception inner) : base(message, inner) { } +// Exception for invalid request format +class InvalidRequestFormatException : Exception { + public InvalidRequestFormatException() { } + public InvalidRequestFormatException(string message) : base(message) { } + public InvalidRequestFormatException(string message, Exception inner) : base(message, inner) { } } -/// -/// Thrown when the API response indicates that the coin was not found. -/// Generally used when the PCGS number and grade combo that was requested does not exist in the PCGS database. -/// -public class CoinNotFoundException : Exception { +// Exception for no coin found +class CoinNotFoundException : Exception { public CoinNotFoundException() { } public CoinNotFoundException(string message) : base(message) { } public CoinNotFoundException(string message, Exception inner) : base(message, inner) { } -} - -/// -/// Thrown when the API returns an HTTP error 401. -/// Generally indicates that the API key is invalid. -/// -public class ApiAuthorizationException : Exception { - public ApiAuthorizationException() { } - public ApiAuthorizationException(string message) : base(message) { } - public ApiAuthorizationException(string message, Exception inner) : base(message, inner) { } -} - -public +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/MainWindowViewModel.cs b/PcgsInvUi/ViewModels/MainWindowViewModel.cs index 4bc71af..c4256e8 100644 --- a/PcgsInvUi/ViewModels/MainWindowViewModel.cs +++ b/PcgsInvUi/ViewModels/MainWindowViewModel.cs @@ -41,6 +41,7 @@ public class MainWindowViewModel : ViewModelBase { public Interaction ShowDeleteWindow { get; } public ReactiveCommand ExportCommand { get; } public Interaction ShowExportWindow { get; } + public Interaction ShowErrorWindow { get; } private ViewModelBase _sideContent; private Coin? _selectedCoin; @@ -55,6 +56,9 @@ public class MainWindowViewModel : ViewModelBase { DisplayedList = ConnectedCoinDatabase.Collection; TotalValue = DisplayedList.Sum(x => x.TotalPrice); + // Set up the ability to show the error window. + ShowErrorWindow = new Interaction(); + // Set up the ability to open the delete window. ShowDeleteWindow = new Interaction(); var deleteEnabled = this.WhenAnyValue(x => x.SelectedCoin) @@ -89,7 +93,15 @@ public class MainWindowViewModel : ViewModelBase { // This is the same as the above, but with a different syntax. newViewModel.OkCommand .Subscribe(async requestStructure => { - ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, requestStructure.Item3); + try { + ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, + requestStructure.Item3); + } + catch (Exception e) { + // TODO Error handling - create a popup window. + Console.WriteLine(e.Message); + } + // TODO Clear the text boxes. }); // Change TotalValue anytime SelectedCoin.TotalPrice changes. diff --git a/PcgsInvUi/Views/MainWindow.axaml.cs b/PcgsInvUi/Views/MainWindow.axaml.cs index 7f80fb8..445fa4e 100644 --- a/PcgsInvUi/Views/MainWindow.axaml.cs +++ b/PcgsInvUi/Views/MainWindow.axaml.cs @@ -53,4 +53,11 @@ public partial class MainWindow : ReactiveWindow { throw new Exception("User cancelled the file picker."); } } + + public async Task ShowErrorWindowAsync(InteractionContext interaction) { + var window = new ErrorWindow(); + window.DataContext = interaction.Input; + + await window.ShowDialog(this); + } } \ No newline at end of file -- 2.54.0 From e73f9db02c178830285986e850756174dd617df5 Mon Sep 17 00:00:00 2001 From: lvas463 Date: Wed, 15 Nov 2023 11:34:02 -0500 Subject: [PATCH 03/21] ShowErrorWindow now actually shows the ErrorWindow --- .idea/.idea.pcgs-inv/.idea/avalonia.xml | 1 + PcgsInvUi/PcgsInvUi.csproj | 1 + PcgsInvUi/Program.cs | 4 +-- PcgsInvUi/Services/CoinDatabase.cs | 18 ++++++++--- PcgsInvUi/Services/PcgsClient.cs | 28 ++++++++++------- PcgsInvUi/ViewModels/ErrorWindowViewModel.cs | 22 +++++++++++++ PcgsInvUi/ViewModels/MainWindowViewModel.cs | 33 +++++++++++++------- PcgsInvUi/Views/ErrorWindow.axaml | 13 ++++++++ PcgsInvUi/Views/ErrorWindow.axaml.cs | 11 +++++++ PcgsInvUi/Views/MainWindow.axaml.cs | 3 +- 10 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 PcgsInvUi/ViewModels/ErrorWindowViewModel.cs create mode 100644 PcgsInvUi/Views/ErrorWindow.axaml create mode 100644 PcgsInvUi/Views/ErrorWindow.axaml.cs diff --git a/.idea/.idea.pcgs-inv/.idea/avalonia.xml b/.idea/.idea.pcgs-inv/.idea/avalonia.xml index 94e266f..091e888 100644 --- a/.idea/.idea.pcgs-inv/.idea/avalonia.xml +++ b/.idea/.idea.pcgs-inv/.idea/avalonia.xml @@ -6,6 +6,7 @@ + diff --git a/PcgsInvUi/PcgsInvUi.csproj b/PcgsInvUi/PcgsInvUi.csproj index 1ac7009..0975dd1 100644 --- a/PcgsInvUi/PcgsInvUi.csproj +++ b/PcgsInvUi/PcgsInvUi.csproj @@ -62,6 +62,7 @@ + diff --git a/PcgsInvUi/Program.cs b/PcgsInvUi/Program.cs index e1f58b0..cc1d9e7 100644 --- a/PcgsInvUi/Program.cs +++ b/PcgsInvUi/Program.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.ReactiveUI; using System; +using PcgsInvUi.ViewModels; namespace PcgsInvUi; @@ -9,8 +10,7 @@ class Program { // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/PcgsInvUi/Services/CoinDatabase.cs b/PcgsInvUi/Services/CoinDatabase.cs index a5ec1ac..0856db4 100644 --- a/PcgsInvUi/Services/CoinDatabase.cs +++ b/PcgsInvUi/Services/CoinDatabase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Data.SQLite; +using System.Threading.Tasks; using PcgsInvUi.Models; namespace PcgsInvUi.Services; @@ -109,9 +110,10 @@ public class CoinDatabase { // Execute the command. cmd.ExecuteNonQuery(); + Console.WriteLine("Coin inserted."); } - public async void CreateCoin(int pcgsNumber, string grade, int quantity) { + public async Task CreateCoin(int pcgsNumber, string grade, int quantity) { Console.WriteLine("Creating coin..."); // Create a coin by calling the API, then insert it into the database. // First, check if the coin already exists - if it does, just update the quantity by adding the amount entered. @@ -119,13 +121,21 @@ public class CoinDatabase { if (coin is not null) { coin.Quantity += quantity; SaveCoin("CollectionTable", coin); + return PcgsClient.ErrorType.None; } - else { - // Build a new coin with the API and insert it. - var newCoin = await _pcgsClient.GetCoinFactsByGrade(pcgsNumber, grade); + + // Otherwise, build a new coin with the API and insert it. + var apiResult = await _pcgsClient.GetCoinFactsByGrade(pcgsNumber, grade); + if (apiResult.Item1 == PcgsClient.ErrorType.None) { + var newCoin = apiResult.Item2; newCoin.Quantity = quantity; InsertCoin(newCoin); + Collection.Add(newCoin); + return PcgsClient.ErrorType.None; } + + Console.WriteLine("Error getting coin from API - {0}", apiResult.Item1); + return apiResult.Item1; } public Coin? GetCoin(int pcgsNumber, string grade) { diff --git a/PcgsInvUi/Services/PcgsClient.cs b/PcgsInvUi/Services/PcgsClient.cs index 3e2af44..e4329d2 100644 --- a/PcgsInvUi/Services/PcgsClient.cs +++ b/PcgsInvUi/Services/PcgsClient.cs @@ -11,6 +11,13 @@ using ReactiveUI; namespace PcgsInvUi.Services; public class PcgsClient : ReactiveObject { + public enum ErrorType { + None, + NoCoinFound, + InvalidRequestFormat, + ApiKeyInvalid + } + public int NumApiRequests { get => _numApiRequests; set => this.RaiseAndSetIfChanged(ref _numApiRequests, value); @@ -26,7 +33,7 @@ public class PcgsClient : ReactiveObject { "E8xivTVN_k4Q8ubk8Xv_uU11U91WX0wDht4S4m4BAf2HPmKnGrZ0J2f9nccM7Ns1VkGoT1SXYR74I7aNaMKqXlJGEy3oobseKV0nGiqu1IKvFkOTVjhV37SWUVExSDFMC6FLzs7xJQIHwkcxr5JFOQNmc4ZytEh_-hmCN3NL-z0fEk98tzBmPJ_F7bq_xNru86QMD2A9yalu_dWe-lCysfyE2bCA2EDqwvWenRYf-0XnNtp6qlxxCMlCd-TDqqRKLC3qefCwpXSemwBiNSP5gbC9pwcKrZugkruuJ0nyxxThZymm"; } - public async Task GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { + public async Task<(ErrorType, Coin?)> GetCoinFactsByGrade(int pcgsNumber, string grade, bool plusGrade = false) { Coin newCoin = null; using (HttpClient client = new HttpClient()) { var gradeParam = GetGradeFromString(grade); @@ -40,15 +47,12 @@ public class PcgsClient : ReactiveObject { if (response.IsSuccessStatusCode) { string jsonResponse = await response.Content.ReadAsStringAsync(); - newCoin = ParseJsonResponse(jsonResponse); - Console.WriteLine("Coin created."); - } - else { - Console.WriteLine($"Error: {response.StatusCode} - {response.ReasonPhrase}"); + var parsedResponse = ParseJsonResponse(jsonResponse); + return parsedResponse; } } - - return newCoin; + + return (ErrorType.ApiKeyInvalid, null); } private int GetGradeFromString(string grade) { @@ -59,14 +63,14 @@ public class PcgsClient : ReactiveObject { // TODO Will eventually create the other methods that are included in the Public API. - private Coin? ParseJsonResponse(string json) { + private (ErrorType, Coin?) ParseJsonResponse(string json) { Console.WriteLine(json); // TODO Check for errors in the response - if (json.Contains("\"ServerMessage\":\"No data found\"")) throw new CoinNotFoundException("No coin found with that PCGS number."); - if (json.Contains("\"IsValidRequest\":true")) throw new InvalidRequestFormatException("Invalid request format."); + if (json.Contains("\"ServerMessage\":\"No data found\"")) return (ErrorType.NoCoinFound, null); + if (json.Contains("\"IsValidRequest\":false")) return (ErrorType.InvalidRequestFormat, null); var newCoin = JsonSerializer.Deserialize(json); - return newCoin; + return (ErrorType.None, newCoin); } } diff --git a/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs b/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs new file mode 100644 index 0000000..116b3d2 --- /dev/null +++ b/PcgsInvUi/ViewModels/ErrorWindowViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Reactive; +using ReactiveUI; +using PcgsInvUi.Views; + +namespace PcgsInvUi.ViewModels; + +public class ErrorWindowViewModel : ViewModelBase { + + public Exception DisplayedException { + get => _message; + set => this.RaiseAndSetIfChanged(ref _message, value); + } + + private Exception _message; + + public ReactiveCommand CloseCommand { get; } + + public ErrorWindowViewModel(string message) { + CloseCommand = ReactiveCommand.Create(window => { window.Close(); }); + } +} \ No newline at end of file diff --git a/PcgsInvUi/ViewModels/MainWindowViewModel.cs b/PcgsInvUi/ViewModels/MainWindowViewModel.cs index c4256e8..05d3433 100644 --- a/PcgsInvUi/ViewModels/MainWindowViewModel.cs +++ b/PcgsInvUi/ViewModels/MainWindowViewModel.cs @@ -41,12 +41,11 @@ public class MainWindowViewModel : ViewModelBase { public Interaction ShowDeleteWindow { get; } public ReactiveCommand ExportCommand { get; } public Interaction ShowExportWindow { get; } - public Interaction ShowErrorWindow { get; } + public Interaction ShowErrorWindow { get; } private ViewModelBase _sideContent; private Coin? _selectedCoin; private double _totalValue; - private PcgsClient _pcgsClient; public MainWindowViewModel(CoinDatabase coins) { ConnectedCoinDatabase = coins; @@ -57,7 +56,7 @@ public class MainWindowViewModel : ViewModelBase { TotalValue = DisplayedList.Sum(x => x.TotalPrice); // Set up the ability to show the error window. - ShowErrorWindow = new Interaction(); + ShowErrorWindow = new Interaction(); // Set up the ability to open the delete window. ShowDeleteWindow = new Interaction(); @@ -84,8 +83,10 @@ public class MainWindowViewModel : ViewModelBase { catch (IOException e) { // Something is wrong with the file after it's been created. Console.WriteLine(e.Message); + ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message)); } catch (Exception e) { Console.WriteLine(e.Message); + ShowErrorWindow.Handle(new ErrorWindowViewModel(e.Message)); } }); @@ -93,19 +94,29 @@ public class MainWindowViewModel : ViewModelBase { // This is the same as the above, but with a different syntax. newViewModel.OkCommand .Subscribe(async requestStructure => { - try { - ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, - requestStructure.Item3); - } - catch (Exception e) { - // TODO Error handling - create a popup window. - Console.WriteLine(e.Message); + var result = await ConnectedCoinDatabase.CreateCoin(requestStructure.Item1, requestStructure.Item2, + requestStructure.Item3); + + switch (result) { + case PcgsClient.ErrorType.ApiKeyInvalid: + Console.WriteLine("API key is invalid - showing window."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("API key is invalid - authorization failed.")); + break; + case PcgsClient.ErrorType.InvalidRequestFormat: + Console.WriteLine("Request format is invalid - showing window."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("Request format is invalid.")); + break; + case PcgsClient.ErrorType.NoCoinFound: + Console.WriteLine("No coin was found with the given parameters."); + await ShowErrorWindow.Handle(new ErrorWindowViewModel("No coin was found with the given parameters.")); + break; } + // TODO Clear the text boxes. }); // Change TotalValue anytime SelectedCoin.TotalPrice changes. - // Possible null deref - ignored due to null values summing to 0. + // Possible null deref - how do you remove this? this.WhenAnyValue(x => x.SelectedCoin.TotalPrice) .Subscribe(_ => TotalValue = ConnectedCoinDatabase.Collection.Sum(x => x.TotalPrice)); } diff --git a/PcgsInvUi/Views/ErrorWindow.axaml b/PcgsInvUi/Views/ErrorWindow.axaml new file mode 100644 index 0000000..3aa990f --- /dev/null +++ b/PcgsInvUi/Views/ErrorWindow.axaml @@ -0,0 +1,13 @@ + + + +