diff --git a/App.axaml.cs b/App.axaml.cs index 4fea7ed..e7162f1 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using BetterRaid.Extensions; using BetterRaid.Services; using BetterRaid.ViewModels; @@ -13,45 +15,51 @@ namespace BetterRaid; public class App : Application { - private static readonly ServiceCollection Services = []; - private static ServiceProvider? _serviceProvider; - - public static IServiceProvider? ServiceProvider => _serviceProvider; + private ServiceProvider? _serviceProvider; public override void Initialize() { - InitializeServices(); - + _serviceProvider = InitializeServices(); AvaloniaXamlLoader.Load(_serviceProvider, this); } - private void InitializeServices() + private ServiceProvider InitializeServices() { + var Services = new ServiceCollection(); Services.AddSingleton(); - Services.AddTransient(); - - _serviceProvider = Services.BuildServiceProvider(); + Services.AddSingleton(serviceProvider => new DispatcherService(Dispatcher.UIThread)); + Services.AddTransient(); + + return Services.BuildServiceProvider(); } public override void OnFrameworkInitializationCompleted() { BindingPlugins.DataValidators.RemoveAt(0); - + + if(_serviceProvider == null) + { + throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null"); + } + + var viewModelFactory = _serviceProvider.GetRequiredService(); + var mainWindowViewModel = viewModelFactory.CreateMainWindowViewModel(); + var mainWindow = new MainWindow() + { + DataContext = mainWindowViewModel + }; + switch (ApplicationLifetime) { case IClassicDesktopStyleApplicationLifetime desktop: - desktop.MainWindow = new MainWindow(); - desktop.MainWindow.InjectDataContext(); - + desktop.MainWindow = mainWindow; break; - + case ISingleViewApplicationLifetime singleViewPlatform: - singleViewPlatform.MainView = new MainWindow(); - singleViewPlatform.MainView.InjectDataContext(); - + singleViewPlatform.MainView = mainWindow; break; } - + base.OnFrameworkInitializationCompleted(); } } \ No newline at end of file diff --git a/Extensions/DataContextExtensions.cs b/Extensions/DataContextExtensions.cs deleted file mode 100644 index 527528d..0000000 --- a/Extensions/DataContextExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Microsoft.Extensions.DependencyInjection; - -namespace BetterRaid.Extensions; - -public static class DataContextExtensions -{ - public static T? GetDataContextAs(this T obj) where T : StyledElement - { - return obj.DataContext as T; - } - - public static void InjectDataContext(this StyledElement e) where T : class - { - if (App.ServiceProvider == null) - return; - - var vm = App.ServiceProvider.GetRequiredService(); - e.DataContext = vm; - } -} \ No newline at end of file diff --git a/Misc/Tools.cs b/Misc/Tools.cs index 75946e9..f53d550 100644 --- a/Misc/Tools.cs +++ b/Misc/Tools.cs @@ -15,7 +15,7 @@ public static class Tools private static HttpListener? _oauthListener; // Source: https://stackoverflow.com/a/43232486 - public static void StartOAuthLogin(string url, Action? callback = null, CancellationToken token = default) + public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default) { if (_oauthListener == null) { @@ -23,13 +23,13 @@ public static class Tools _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/"); _oauthListener.Start(); - Task.Run(() => WaitForCallback(callback, token), token); + Task.Run(() => WaitForCallback(callback, token, twitchService), token); } - OpenUrl(url); + OpenUrl(twitchService.GetOAuthUrl()); } - private static async Task WaitForCallback(Action? callback, CancellationToken token) + private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService) { if (_oauthListener == null) return; @@ -107,11 +107,7 @@ public static class Tools var accessToken = jsonData["access_token"]?.ToString(); - var dataService = App.ServiceProvider?.GetService(typeof(ITwitchService)); - if (dataService is ITwitchService twitchDataService) - { - twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!); - } + twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!); res.StatusCode = 200; res.Close(); diff --git a/Services/IMainViewModelFactory.cs b/Services/IMainViewModelFactory.cs new file mode 100644 index 0000000..1ec41b3 --- /dev/null +++ b/Services/IMainViewModelFactory.cs @@ -0,0 +1,8 @@ +using BetterRaid.ViewModels; + +namespace BetterRaid.Services; + +public interface IMainViewModelFactory +{ + MainWindowViewModel CreateMainWindowViewModel(); +} diff --git a/Services/ISynchronizaionService.cs b/Services/ISynchronizaionService.cs new file mode 100644 index 0000000..92f7aa8 --- /dev/null +++ b/Services/ISynchronizaionService.cs @@ -0,0 +1,8 @@ +using System; + +namespace BetterRaid.Services; + +public interface ISynchronizaionService +{ + void Invoke(Action action); +} diff --git a/Services/Implementations/DispatcherService.cs b/Services/Implementations/DispatcherService.cs new file mode 100644 index 0000000..124215e --- /dev/null +++ b/Services/Implementations/DispatcherService.cs @@ -0,0 +1,18 @@ +using Avalonia.Threading; +using System; +using System.Threading; + +namespace BetterRaid.Services.Implementations; +public class DispatcherService : ISynchronizaionService +{ + private readonly Dispatcher dispatcher; + + public DispatcherService(Dispatcher dispatcher) + { + this.dispatcher = dispatcher; + } + public void Invoke(Action action) + { + dispatcher.Invoke(action); + } +} diff --git a/Services/Implementations/MainWindowViewModelFactory.cs b/Services/Implementations/MainWindowViewModelFactory.cs new file mode 100644 index 0000000..7e10ab5 --- /dev/null +++ b/Services/Implementations/MainWindowViewModelFactory.cs @@ -0,0 +1,22 @@ +using BetterRaid.ViewModels; + +namespace BetterRaid.Services.Implementations; + +public class MainWindowViewModelFactory : IMainViewModelFactory +{ + private readonly ITwitchPubSubService twitchPubSubService; + private readonly ITwitchDataService twitchDataService; + private readonly ISynchronizaionService synchronizaionService; + + public MainWindowViewModelFactory(ITwitchPubSubService twitchPubSubService, ITwitchDataService twitchDataService, ISynchronizaionService synchronizaionService) + { + this.twitchPubSubService = twitchPubSubService; + this.twitchDataService = twitchDataService; + this.synchronizaionService = synchronizaionService; + } + + public MainWindowViewModel CreateMainWindowViewModel() + { + return new MainWindowViewModel(twitchPubSubService, twitchDataService, synchronizaionService); + } +} diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 9d7c292..c3941d6 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -18,6 +18,7 @@ public class MainWindowViewModel : ViewModelBase private string? _filter; private ObservableCollection _channels = []; private readonly BetterRaidDatabase? _db; + private readonly ISynchronizaionService _synchronizaionService; public BetterRaidDatabase? Database { @@ -36,7 +37,7 @@ public class MainWindowViewModel : ViewModelBase get => _channels; set => SetProperty(ref _channels, value); } - + public ObservableCollection FilteredChannels => GetFilteredChannels(); public ITwitchService Twitch { get; } @@ -49,8 +50,10 @@ public class MainWindowViewModel : ViewModelBase public bool IsLoggedIn => Twitch.UserChannel != null; - public MainWindowViewModel(ITwitchService twitch) + public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService) { + _synchronizaionService = synchronizaionService; + Twitch = twitch; Twitch.PropertyChanged += OnTwitchPropertyChanged; @@ -73,7 +76,7 @@ public class MainWindowViewModel : ViewModelBase public void LoginWithTwitch() { - Tools.StartOAuthLogin(Twitch.GetOAuthUrl(), OnTwitchLoginCallback, CancellationToken.None); + Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None); } private void OnTwitchLoginCallback() @@ -87,18 +90,18 @@ public class MainWindowViewModel : ViewModelBase { return; } - + foreach (var channel in Channels) { Twitch.UnregisterFromEvents(channel); } - + Channels.Clear(); var channels = _db.Channels .Select(channelName => new TwitchChannel(channelName)) .ToList(); - + foreach (var channel in channels) { Task.Run(() => @@ -106,7 +109,7 @@ public class MainWindowViewModel : ViewModelBase channel.UpdateChannelData(Twitch); Twitch.RegisterForEvents(channel); }); - + Channels.Add(channel); } } @@ -117,7 +120,7 @@ public class MainWindowViewModel : ViewModelBase .Where(channel => Database?.OnlyOnline == false || channel.IsLive) .Where(channel => string.IsNullOrWhiteSpace(Filter) || channel.Name?.Contains(Filter, StringComparison.OrdinalIgnoreCase) == true) .ToList(); - + return new ObservableCollection(filteredChannels); } @@ -125,14 +128,14 @@ public class MainWindowViewModel : ViewModelBase { if (e.PropertyName != nameof(Twitch.UserChannel)) return; - + OnPropertyChanged(nameof(IsLoggedIn)); } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); - + if (e.PropertyName == nameof(Filter)) { OnPropertyChanged(nameof(FilteredChannels)); @@ -143,7 +146,7 @@ public class MainWindowViewModel : ViewModelBase { if (e.PropertyName != nameof(BetterRaidDatabase.OnlyOnline)) return; - + OnPropertyChanged(nameof(FilteredChannels)); } }