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..90d64d1 100644 --- a/src/ArcadePointsBot/App.axaml.cs +++ b/src/ArcadePointsBot/App.axaml.cs @@ -31,20 +31,11 @@ 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() + private void EnsureDb(IServiceProvider services) { - using var scope = _services.CreateScope(); + using var scope = services.CreateScope(); var loggerFactory = scope.ServiceProvider.GetRequiredService(); var logger = loggerFactory.CreateLogger("App"); logger.LogInformation("Checking for Db updates"); @@ -67,14 +58,13 @@ private void EnsureDb() 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(); + EnsureDb(GlobalHost.Services); 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/ViewLocator.cs b/src/ArcadePointsBot/ViewLocator.cs index b431b90..c9bfc42 100644 --- a/src/ArcadePointsBot/ViewLocator.cs +++ b/src/ArcadePointsBot/ViewLocator.cs @@ -3,38 +3,32 @@ using AvaloniaApplication1.ViewModels; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; +using System.Linq; namespace AvaloniaApplication1 { - public class ViewLocator : IDataTemplate + public static class ViewLocatorHelpers { - private readonly IServiceProvider _serviceProvider; - - public ViewLocator(IServiceProvider serviceProvider) + public static IServiceCollection AddView(this IServiceCollection services) + where TView : Control, new() + where TViewModel : ViewModelBase { - _serviceProvider = serviceProvider; + services.AddSingleton(new ViewLocator.ViewLocationDescriptor(typeof(TViewModel), () => new TView())); + return services; } + } + public class ViewLocator : IDataTemplate + { + private readonly Dictionary> _views; - public Control? Build(object? data) + public ViewLocator(IEnumerable descriptors) { - if(data is null) - return new TextBlock { Text = "Not Found" }; - - var name = data.GetType().FullName!.Replace("ViewModel", "View"); - var type = Type.GetType(name); - - if (type != null) - { - var scope = _serviceProvider.CreateScope(); - return (Control)scope.ServiceProvider.GetRequiredService(type)!; - } - - return new TextBlock { Text = "Not Found: " + name }; + _views = descriptors.ToDictionary(x => x.ViewModel, x => x.Factory); } - public bool Match(object? data) - { - return data is ViewModelBase; - } + public Control Build(object? param) => _views[param!.GetType()](); + public bool Match(object? param) => param is not null && _views.ContainsKey(param.GetType()); + public record ViewLocationDescriptor(Type ViewModel, Func Factory); } } \ No newline at end of file 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/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()); +} 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; } 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 -