From 94946a37a3530707953019a2fcf01ac7bfea160b Mon Sep 17 00:00:00 2001 From: ArcadeArchie <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:36:42 +0100 Subject: [PATCH 1/5] Moved Host into App Moved the .NET Host into `App.axaml.cs` and replaced the old DesignTime provider with a new one --- src/ArcadePointsBot/App.axaml | 22 ++- src/ArcadePointsBot/App.axaml.cs | 147 +++++++++++++----- src/ArcadePointsBot/Program.cs | 124 ++------------- .../Util/DesignTimeServices.cs | 21 --- src/ArcadePointsBot/ViewModels/DesignData.cs | 25 +++ 5 files changed, 161 insertions(+), 178 deletions(-) delete mode 100644 src/ArcadePointsBot/Util/DesignTimeServices.cs create mode 100644 src/ArcadePointsBot/ViewModels/DesignData.cs diff --git a/src/ArcadePointsBot/App.axaml b/src/ArcadePointsBot/App.axaml index 8e34c72..1d0c081 100644 --- a/src/ArcadePointsBot/App.axaml +++ b/src/ArcadePointsBot/App.axaml @@ -1,13 +1,27 @@ - - - + + Stopped + Errored + Starting + Running + + Press + Keyboard + Mouse + + M8.36612 16.1161C7.87796 16.6043 7.87796 17.3957 8.36612 17.8839L23.1161 32.6339C23.6043 33.122 24.3957 33.122 24.8839 32.6339L39.6339 17.8839C40.122 17.3957 40.122 16.6043 39.6339 16.1161C39.1457 15.628 38.3543 15.628 37.8661 16.1161L24 29.9822L10.1339 16.1161C9.64573 15.628 8.85427 15.628 8.36612 16.1161Z + M39.6339 31.8839C39.1457 32.372 38.3543 32.372 37.8661 31.8839L24 18.0178L10.1339 31.8839C9.64573 32.372 8.85427 32.372 8.36612 31.8839C7.87796 31.3957 7.87796 30.6043 8.36612 30.1161L23.1161 15.3661C23.6043 14.878 24.3957 14.878 24.8839 15.3661L39.6339 30.1161C40.122 30.6043 40.122 31.3957 39.6339 31.8839Z + M2 9.5C2 9.22386 2.22386 9 2.5 9H17.5C17.7761 9 18 9.22386 18 9.5C18 9.77614 17.7761 10 17.5 10H2.5C2.22386 10 2 9.77614 2 9.5Z + + diff --git a/src/ArcadePointsBot/App.axaml.cs b/src/ArcadePointsBot/App.axaml.cs index da78faf..612369a 100644 --- a/src/ArcadePointsBot/App.axaml.cs +++ b/src/ArcadePointsBot/App.axaml.cs @@ -31,50 +31,40 @@ namespace AvaloniaApplication1; public partial class App : Application { - private readonly IServiceProvider _services; + public IHost? GlobalHost { get; private set; } - - public App(IServiceProvider services) - { - _services = services; - if (!Design.IsDesignMode) - EnsureDb(); - RxApp.DefaultExceptionHandler = _services.GetRequiredService>(); - } - - private void EnsureDb() - { - using var scope = _services.CreateScope(); - var loggerFactory = scope.ServiceProvider.GetRequiredService(); - var logger = loggerFactory.CreateLogger("App"); - logger.LogInformation("Checking for Db updates"); - try - { - using var db = scope.ServiceProvider.GetRequiredService(); - var migrations = db.Database.GetPendingMigrations(); - if (migrations.Any()) - { - logger.LogInformation("Found {count} updates, updating DB", migrations.Count()); - db.Database.Migrate(); - } - } - catch (Exception ex) - { - logger.LogError(ex, "An Error occurred updating the DB"); - throw; - } - } + //private void EnsureDb() + //{ + // using var scope = Services.CreateScope(); + // var loggerFactory = scope.ServiceProvider.GetRequiredService(); + // var logger = loggerFactory.CreateLogger("App"); + // logger.LogInformation("Checking for Db updates"); + // try + // { + // using var db = scope.ServiceProvider.GetRequiredService(); + // var migrations = db.Database.GetPendingMigrations(); + // if (migrations.Any()) + // { + // logger.LogInformation("Found {count} updates, updating DB", migrations.Count()); + // db.Database.Migrate(); + // } + // } + // catch (Exception ex) + // { + // logger.LogError(ex, "An Error occurred updating the DB"); + // throw; + // } + //} public override void Initialize() { - AvaloniaXamlLoader.Load(_services, this); - - Resources[typeof(IServiceProvider)] = _services; - DataTemplates.Add(_services.GetRequiredService()); + AvaloniaXamlLoader.Load(this); } - public override void OnFrameworkInitializationCompleted() + public override async void OnFrameworkInitializationCompleted() { + GlobalHost = CreateAppBuilder().Build(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { // Line below is needed to remove Avalonia data validation. @@ -83,10 +73,91 @@ public override void OnFrameworkInitializationCompleted() desktop.MainWindow = new MainWindow { - DataContext = this.CreateInstance(), + DataContext = GlobalHost.Services.GetRequiredService(), + }; + desktop.Exit += (_, _) => + { + GlobalHost.StopAsync().GetAwaiter().GetResult(); + GlobalHost.Dispose(); + GlobalHost = null; }; } + + DataTemplates.Add(GlobalHost.Services.GetRequiredService()); base.OnFrameworkInitializationCompleted(); + + await GlobalHost.StartAsync(); } + + + public static IHostBuilder CreateAppBuilder() => Host + .CreateDefaultBuilder(Environment.GetCommandLineArgs()) + .UseSerilog((ctx, svc, cfg) => + { + cfg + .ReadFrom.Configuration(ctx.Configuration) + .ReadFrom.Services(svc) + .Enrich.FromLogContext(); + }) + .ConfigureAppConfiguration(WithApplicationConfiguration) + .ConfigureServices(WithApplicationServices); + + private static void WithApplicationConfiguration(HostBuilderContext context, IConfigurationBuilder configurationBuilder) + { + if (Design.IsDesignMode) + return; + configurationBuilder.Sources.Clear(); + configurationBuilder + .SetBasePath(Directory.GetCurrentDirectory()) + .Add(s => + { + s.Path = "appsettings.json"; + s.Optional = false; + s.ReloadOnChange = true; + s.FileProvider = null; + s.ResolveFileProvider(); + }) + .AddJsonFile("appsettings.Development.json", true, true); + + if (context.HostingEnvironment.IsDevelopment()) + { + configurationBuilder.AddUserSecrets(Assembly.GetExecutingAssembly()); + } + + configurationBuilder.AddEnvironmentVariables(); + } + private static void WithApplicationServices(HostBuilderContext context, IServiceCollection services) + { + services.AddDbContext(options => + { + var dbPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ArcadePointsBot.db"); + options.UseSqlite($"Data Source={dbPath}"); + }); + + services.AddOptions().BindConfiguration("TwitchAuthConfig"); + + services.Configure(options => + { + var rawConfig = context.Configuration; + options.PropertyChanged += (o, e) => + { + if (e is not PropertyChangedEventArgsEx args) throw new InvalidOperationException(); + rawConfig["TwitchAuthConfig:" + args.PropertyName!] = args.Value?.ToString(); + }; + }); + + services.AddSingleton, GlobalRxExceptionHandler>(); + services.AddSingleton(); + services.AddScoped(); + + services.AddScoped(typeof(IEntityRepository<,>), typeof(DataEntityRepository<,>)); + services.AddScoped(); + + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } } \ No newline at end of file diff --git a/src/ArcadePointsBot/Program.cs b/src/ArcadePointsBot/Program.cs index a4f690f..67edfac 100644 --- a/src/ArcadePointsBot/Program.cs +++ b/src/ArcadePointsBot/Program.cs @@ -16,8 +16,10 @@ using Microsoft.Extensions.Logging; using Serilog; using System; +using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace AvaloniaApplication1 @@ -29,123 +31,15 @@ internal class Program // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static async Task Main(string[] args) - { - var host = CreateAppBuilder(args).Build(); - await host.StartAsync(); - ServiceProvider = host.Services; - var app = BuildAvaloniaAppWithServices(host.Services); - try - { - var res = app.StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); - await host.StopAsync(); - - return res; - } - catch (Exception ex) - { - Log.Fatal(ex, "Something very bad happened, please send this log to the devs"); - return -1; - } - //BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } - - - public static IHostBuilder CreateAppBuilder(string[] args) => Host - .CreateDefaultBuilder(args) - .UseSerilog((ctx, svc, cfg) => - { - cfg - .ReadFrom.Configuration(ctx.Configuration) - .ReadFrom.Services(svc) - .Enrich.FromLogContext(); - }) - .ConfigureAppConfiguration(WithApplicationConfiguration) - .ConfigureServices(WithApplicationServices); - - private static void WithApplicationConfiguration(HostBuilderContext context, IConfigurationBuilder configurationBuilder) - { - if (Design.IsDesignMode) - return; - configurationBuilder.Sources.Clear(); - configurationBuilder - .SetBasePath(Directory.GetCurrentDirectory()) - .Add(s => - { - s.Path = "appsettings.json"; - s.Optional = false; - s.ReloadOnChange = true; - s.FileProvider = null; - s.ResolveFileProvider(); - }) - .AddJsonFile("appsettings.Development.json", true, true); - - if (context.HostingEnvironment.IsDevelopment()) - { - configurationBuilder.AddUserSecrets(Assembly.GetExecutingAssembly()); - } - - configurationBuilder.AddEnvironmentVariables(); - } - private static void WithApplicationServices(HostBuilderContext context, IServiceCollection services) - { - //if (context.HostingEnvironment.IsDevelopment()) - //{ - // services.AddDbContext(options => - // { - // var connString = "Server=192.168.178.28; User ID=root; Password=31401577; Database=twitchTest"; - // options.UseMySql(connString, ServerVersion.AutoDetect(connString)); - // }); - //} - //else - //{ - services.AddDbContext(options => - { - var dbPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ArcadePointsBot.db"); - options.UseSqlite($"Data Source={dbPath}"); - }); - //} - - //services.Configure(section); - services.AddOptions().BindConfiguration("TwitchAuthConfig"); - - services.Configure(options => - { - var rawConfig = context.Configuration; - options.PropertyChanged += (o, e) => - { - if (e is not PropertyChangedEventArgsEx args) throw new InvalidOperationException(); - rawConfig["TwitchAuthConfig:" + args.PropertyName!] = args.Value?.ToString(); - }; - }); - - services.AddSingleton, GlobalRxExceptionHandler>(); - services.AddSingleton(); - services.AddSingleton(); - services.AddScoped(); - - services.AddScoped(typeof(IEntityRepository<,>), typeof(DataEntityRepository<,>)); - services.AddScoped(); - - services.AddSingleton(); - - services.AddScoped(); - services.AddScoped(); - } - - public static AppBuilder BuildAvaloniaAppWithServices(IServiceProvider services) => AppBuilder - .Configure(() => new App(services)) - .UsePlatformDetect() - .UseReactiveUI() - .WithInterFont() - .LogToTrace(); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() - { - var host = Host.CreateDefaultBuilder().ConfigureServices(WithApplicationServices).Build(); - Program.ServiceProvider = host.Services; - return BuildAvaloniaAppWithServices(host.Services); - } + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); } } diff --git a/src/ArcadePointsBot/Util/DesignTimeServices.cs b/src/ArcadePointsBot/Util/DesignTimeServices.cs deleted file mode 100644 index f0917d5..0000000 --- a/src/ArcadePointsBot/Util/DesignTimeServices.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AvaloniaApplication1.Util; - -public static class DesignTimeServices -{ - private static IServiceProvider? rawServices; - - private static readonly Lazy Scope = new(() => - { - rawServices = Program.ServiceProvider; - return rawServices.CreateScope(); - }); - - public static IServiceProvider Services => Scope.Value.ServiceProvider; -} diff --git a/src/ArcadePointsBot/ViewModels/DesignData.cs b/src/ArcadePointsBot/ViewModels/DesignData.cs new file mode 100644 index 0000000..d31210d --- /dev/null +++ b/src/ArcadePointsBot/ViewModels/DesignData.cs @@ -0,0 +1,25 @@ +using Avalonia; +using AvaloniaApplication1; +using AvaloniaApplication1.Services; +using AvaloniaApplication1.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; +using System.Threading; + +namespace ArcadePointsBot; + +/// +/// Used by for the Designer as a source of generated view models +/// +public static class DesignData +{ + public static MainWindowViewModel MainWindowViewModel { get; } = + ((App)Application.Current!).GlobalHost!.Services.GetRequiredService(); + + + public static CreateRewardWindowViewModel CreateRewardWindowViewModel { get; } = + ((App)Application.Current!).GlobalHost!.Services.GetRequiredService(); + + public static EditRewardViewModel EditRewardViewModel { get; } = new EditRewardViewModel(null, + new AvaloniaApplication1.Models.TwitchReward()); +} From 0e39750831932faa21e347c04585325df996c2d2 Mon Sep 17 00:00:00 2001 From: ArcadeArchie <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:38:04 +0100 Subject: [PATCH 2/5] Changed VMs to use a service scope needed for DesignTime data --- .../ViewModels/CreateRewardWindowViewModel.cs | 10 ++--- .../ViewModels/EditRewardViewModel.cs | 4 -- .../ViewModels/MainWindowViewModel.cs | 37 ++++++++----------- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/ArcadePointsBot/ViewModels/CreateRewardWindowViewModel.cs b/src/ArcadePointsBot/ViewModels/CreateRewardWindowViewModel.cs index 166c924..d622a29 100644 --- a/src/ArcadePointsBot/ViewModels/CreateRewardWindowViewModel.cs +++ b/src/ArcadePointsBot/ViewModels/CreateRewardWindowViewModel.cs @@ -17,6 +17,7 @@ namespace AvaloniaApplication1.ViewModels { public partial class CreateRewardWindowViewModel : ViewModelBase { + private readonly IServiceScope _serviceScope; private readonly TwitchPointRewardService _pointRewardService; [Reactive] @@ -41,9 +42,10 @@ public partial class CreateRewardWindowViewModel : ViewModelBase [ObservableAsProperty] public bool HasActions { get; } - public CreateRewardWindowViewModel(TwitchPointRewardService pointRewardService) + public CreateRewardWindowViewModel(IServiceProvider serviceProvider) { - _pointRewardService = pointRewardService; + _serviceScope = serviceProvider.CreateScope(); + _pointRewardService = _serviceScope.ServiceProvider.GetRequiredService(); Actions.ToObservableChangeSet(x => x).ToCollection().Select(x => x.Any()).ToPropertyEx(this, x => x.HasActions); CreateTwitchRewardCommand = ReactiveCommand.CreateFromTask(CreateTwitchReward, Observable.CombineLatest( @@ -57,10 +59,6 @@ public CreateRewardWindowViewModel(TwitchPointRewardService pointRewardService) RemoveActionCommand = ReactiveCommand.Create(action => Actions.Remove(action)); } - - public CreateRewardWindowViewModel() : this(DesignTimeServices.Services.GetRequiredService()) - { - } void DuplicateAction(RewardActionViewModel action) { var dupedAction = new RewardActionViewModel(Actions.Count) diff --git a/src/ArcadePointsBot/ViewModels/EditRewardViewModel.cs b/src/ArcadePointsBot/ViewModels/EditRewardViewModel.cs index b36a0d0..b6951f2 100644 --- a/src/ArcadePointsBot/ViewModels/EditRewardViewModel.cs +++ b/src/ArcadePointsBot/ViewModels/EditRewardViewModel.cs @@ -67,10 +67,6 @@ public EditRewardViewModel(TwitchPointRewardService pointRewardService, TwitchRe RemoveActionCommand = ReactiveCommand.Create(action => Actions.Remove(action)); } - - public EditRewardViewModel() : this(DesignTimeServices.Services.GetRequiredService(), new TwitchReward()) - { - } void DuplicateAction(RewardActionViewModel action) { var dupedAction = new RewardActionViewModel(Actions.Count) diff --git a/src/ArcadePointsBot/ViewModels/MainWindowViewModel.cs b/src/ArcadePointsBot/ViewModels/MainWindowViewModel.cs index 797e999..fd0174a 100644 --- a/src/ArcadePointsBot/ViewModels/MainWindowViewModel.cs +++ b/src/ArcadePointsBot/ViewModels/MainWindowViewModel.cs @@ -4,7 +4,6 @@ using AvaloniaApplication1.Auth; using AvaloniaApplication1.Models; using AvaloniaApplication1.Services; -using AvaloniaApplication1.Util; using AvaloniaApplication1.Views; using DynamicData; using Microsoft.EntityFrameworkCore; @@ -12,6 +11,7 @@ using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -23,7 +23,7 @@ namespace AvaloniaApplication1.ViewModels { public partial class MainWindowViewModel : ViewModelBase { - private readonly ILogger _logger; + private readonly IServiceScope _serviceScope; private readonly IAuthenticationService _authenticationService; private readonly TwitchPointRewardService _rewardService; private readonly TwitchWorker _worker; @@ -38,42 +38,37 @@ public bool IsAuthed set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref isAuthed, value)); } private ObservableCollection _rewardList = new(); - public DataGridCollectionView Rewards { get; set; } + public DataGridCollectionView? Rewards { get; set; } public ReactiveCommand CreateRewardCommand { get; set; } public ReactiveCommand EditRewardCommand { get; set; } public ReactiveCommand DeleteRewardCommand { get; set; } - public MainWindowViewModel(ILogger logger, - IAuthenticationService authenticationService, TwitchPointRewardService rewardService, TwitchWorker worker) + public MainWindowViewModel(IServiceProvider serviceProvider, TwitchWorker worker) { - _logger = logger; - _authenticationService = authenticationService; + _serviceScope = serviceProvider.CreateScope(); + _authenticationService = _serviceScope.ServiceProvider.GetRequiredService(); + _rewardService = _serviceScope.ServiceProvider.GetRequiredService(); StatusText = "Checking Auth status"; + + _worker = worker; + _worker.PropertyChanged += WorkerPropertyChanged; + + + CreateRewardCommand = ReactiveCommand.CreateFromTask(CreateReward, Observable.CombineLatest(IsBusyObservable, this.WhenAny(x => x.IsAuthed, x => x.Value), (isBusy, isAuthed) => isBusy && isAuthed)); EditRewardCommand = ReactiveCommand.CreateFromTask(EditReward, Observable.CombineLatest(IsBusyObservable, this.WhenAny(x => x.IsAuthed, x => x.Value), (isBusy, isAuthed) => isBusy && isAuthed)); DeleteRewardCommand = ReactiveCommand.CreateFromTask(DeleteReward, IsBusyObservable); - - _rewardService = rewardService; - _worker = worker; - _worker.PropertyChanged += _worker_PropertyChanged; } - private void _worker_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + private void WorkerPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName != nameof(_worker.Status)) return; WorkerStatus = _worker.Status; } - public MainWindowViewModel() : this( - DesignTimeServices.Services.GetRequiredService>(), - DesignTimeServices.Services.GetRequiredService(), - DesignTimeServices.Services.GetRequiredService(), - DesignTimeServices.Services.GetRequiredService()) - { } - private async Task CreateReward() { IsBusy = true; @@ -81,7 +76,7 @@ private async Task CreateReward() { var reward = await new CreateRewardWindow() { - DataContext = Avalonia.Application.Current.CreateInstance(), + DataContext = _serviceScope.ServiceProvider.GetRequiredService(), }.ShowDialog(desktop.MainWindow!); if (reward != null) { @@ -110,7 +105,7 @@ private async Task EditReward(TwitchReward reward) private async Task DeleteReward(TwitchReward reward) { IsBusy = true; - Rewards.Remove(reward); + _rewardList.Remove(reward); await _rewardService.DeleteReward(reward); IsBusy = false; } From 709153c696cc2a7e38159a28884c320c1145e4b4 Mon Sep 17 00:00:00 2001 From: ArcadeArchie <29282748+ArcadeArchie@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:38:38 +0100 Subject: [PATCH 3/5] changed DesignTime context refs to new provider --- .../Views/CreateRewardWindow.axaml | 10 ++--- .../Views/EditRewardWindow.axaml | 9 ++--- src/ArcadePointsBot/Views/MainWindow.axaml | 40 ++++++++----------- .../Views/RewardActionView.axaml | 15 +------ 4 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/ArcadePointsBot/Views/CreateRewardWindow.axaml b/src/ArcadePointsBot/Views/CreateRewardWindow.axaml index 558fbf3..2896f23 100644 --- a/src/ArcadePointsBot/Views/CreateRewardWindow.axaml +++ b/src/ArcadePointsBot/Views/CreateRewardWindow.axaml @@ -1,6 +1,7 @@ - - - - + Title="CreateRewardWindow" + Design.DataContext="{x:Static local:DesignData.CreateRewardWindowViewModel}"> + diff --git a/src/ArcadePointsBot/Views/EditRewardWindow.axaml b/src/ArcadePointsBot/Views/EditRewardWindow.axaml index 40e4024..e5536dc 100644 --- a/src/ArcadePointsBot/Views/EditRewardWindow.axaml +++ b/src/ArcadePointsBot/Views/EditRewardWindow.axaml @@ -1,5 +1,6 @@ - - - - + Title="EditRewardWindow" + Design.DataContext="{x:Static local:DesignData.EditRewardViewModel}"> diff --git a/src/ArcadePointsBot/Views/MainWindow.axaml b/src/ArcadePointsBot/Views/MainWindow.axaml index 3aaecc4..04b09cb 100644 --- a/src/ArcadePointsBot/Views/MainWindow.axaml +++ b/src/ArcadePointsBot/Views/MainWindow.axaml @@ -1,5 +1,6 @@ - - - - - - - Stopped - Errored - Starting - Running - - + Title="AvaloniaApplication1" + Design.DataContext="{x:Static local:DesignData.MainWindowViewModel}"> + - - - Enabled -