Skip to content

Commit 0b7b6e3

Browse files
feat: Hooking account information up to DI
Step 1: Get the account information updated and updating in the application.
1 parent 40db741 commit 0b7b6e3

File tree

10 files changed

+306
-91
lines changed

10 files changed

+306
-91
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
# twitch-streaming-tools
22

3-
Tools to aid twitch streamers.
3+
Tools to aid twitch streamers.
4+
5+
## Design
6+
7+
### Account Management
8+
9+
Relationship between the view model and the account manager used to keep Twitch OAuth credentails up-to-date
10+
11+
```mermaid
12+
classDiagram
13+
AccountViewModel o-- IAccountManager
14+
IAccountManager
15+
class AccountViewModel{
16+
-IAccountManager _accountManager
17+
+LaunchOAuthBrowser()
18+
+DeleteCredentials()
19+
}
20+
class IAccountManager{
21+
+bool CredentialsAreValid
22+
+Action<bool> OnCredentialStatusChanged
23+
+void UpdateCredentials(string bearer, string refresh, DateTime expires)
24+
+void DeleteCredentials()
25+
-void CheckCredentials()
26+
}
27+
```

src/TwitchStreamingTools/App.axaml.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
using System;
2-
using System.Threading.Tasks;
32

43
using Avalonia;
54
using Avalonia.Controls;
65
using Avalonia.Controls.ApplicationLifetimes;
76
using Avalonia.Markup.Xaml;
87

8+
using Microsoft.Extensions.DependencyInjection;
9+
910
using Nullinside.Api.Common.Twitch;
1011

1112
using TwitchStreamingTools.Models;
13+
using TwitchStreamingTools.Services;
1214
using TwitchStreamingTools.ViewModels;
1315
using TwitchStreamingTools.Views;
1416

@@ -32,9 +34,16 @@ public override void OnFrameworkInitializationCompleted() {
3234
TwitchClientProxy.Instance.TwitchOAuthToken = Configuration.Instance.OAuth?.Bearer;
3335
TwitchClientProxy.Instance.TwitchUsername = Configuration.Instance.TwitchUsername;
3436

37+
// Register all the services needed for the application to run
38+
var collection = new ServiceCollection();
39+
collection.AddCommonServices();
40+
41+
// Creates a ServiceProvider containing services from the provided IServiceCollection
42+
ServiceProvider services = collection.BuildServiceProvider();
43+
var vm = services.GetRequiredService<MainWindowViewModel>();
3544
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
3645
desktop.MainWindow = new MainWindow {
37-
DataContext = new MainWindowViewModel()
46+
DataContext = vm
3847
};
3948
}
4049

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
using Avalonia.Threading;
5+
6+
using Nullinside.Api.Common.Twitch;
7+
8+
using TwitchStreamingTools.Models;
9+
10+
namespace TwitchStreamingTools.Services;
11+
12+
/// <summary>
13+
/// Manages the credentials in the application.
14+
/// </summary>
15+
public class AccountManager : IAccountManager {
16+
/// <summary>
17+
/// The timer used to check the twitch OAuth token against the API.
18+
/// </summary>
19+
private readonly DispatcherTimer _timer;
20+
21+
/// <summary>
22+
/// The twitch chat api.
23+
/// </summary>
24+
private readonly ITwitchApiProxy _twitchApi;
25+
26+
/// <summary>
27+
/// The twitch chat client.
28+
/// </summary>
29+
private readonly ITwitchClientProxy _twitchClient;
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="AccountManager" /> class.
33+
/// </summary>
34+
/// <param name="twitchClient">The twitch chat client.</param>
35+
/// <param name="twitchApi">The twitch chat api.</param>
36+
public AccountManager(ITwitchClientProxy twitchClient, ITwitchApiProxy twitchApi) {
37+
_twitchClient = twitchClient;
38+
_twitchApi = twitchApi;
39+
_timer = new DispatcherTimer {
40+
Interval = TimeSpan.FromSeconds(5)
41+
};
42+
43+
_timer.Tick += async (_, _) => await OnCheckCredentials();
44+
_ = OnCheckCredentials();
45+
}
46+
47+
/// <inheritdoc />
48+
public string? TwitchUsername { get; set; }
49+
50+
/// <inheritdoc />
51+
public bool CredentialsAreValid { get; set; }
52+
53+
/// <inheritdoc />
54+
public Action<bool>? OnCredentialsStatusChanged { get; set; }
55+
56+
/// <inheritdoc />
57+
public Action<TwitchAccessToken?>? OnCredentialsChanged { get; set; }
58+
59+
/// <inheritdoc />
60+
public async Task UpdateCredentials(string bearer, string refresh, DateTime expires) {
61+
_twitchApi.OAuth = new TwitchAccessToken {
62+
AccessToken = bearer,
63+
RefreshToken = refresh,
64+
ExpiresUtc = expires
65+
};
66+
67+
(string? id, string? username)? user = null;
68+
try {
69+
user = await _twitchApi.GetUser();
70+
}
71+
catch {
72+
// Do nothing
73+
}
74+
75+
Configuration.Instance.OAuth = new OAuthResponse {
76+
Bearer = bearer,
77+
Refresh = refresh,
78+
ExpiresUtc = expires
79+
};
80+
81+
Configuration.Instance.TwitchUsername = user?.username;
82+
Configuration.Instance.WriteConfiguration();
83+
_twitchClient.TwitchOAuthToken = bearer;
84+
_twitchClient.TwitchUsername = user?.username;
85+
86+
OnCredentialsChanged?.Invoke(null);
87+
await OnCheckCredentials();
88+
}
89+
90+
/// <inheritdoc />
91+
public void DeleteCredentials() {
92+
_twitchApi.OAuth = null;
93+
Configuration.Instance.OAuth = null;
94+
Configuration.Instance.TwitchUsername = null;
95+
_twitchClient.TwitchOAuthToken = null;
96+
_twitchClient.TwitchUsername = null;
97+
CredentialsAreValid = false;
98+
TwitchUsername = null;
99+
100+
OnCredentialsChanged?.Invoke(null);
101+
OnCredentialsStatusChanged?.Invoke(false);
102+
}
103+
104+
/// <summary>
105+
/// Checks the OAuth token against the API to verify its validity.
106+
/// </summary>
107+
private async Task OnCheckCredentials() {
108+
_timer.Stop();
109+
try {
110+
bool previousValue = CredentialsAreValid;
111+
string? username = (await _twitchApi.GetUser()).username;
112+
CredentialsAreValid = !string.IsNullOrWhiteSpace(username);
113+
TwitchUsername = username;
114+
115+
if (previousValue != CredentialsAreValid) {
116+
OnCredentialsStatusChanged?.Invoke(CredentialsAreValid);
117+
}
118+
}
119+
catch {
120+
// Do nothing
121+
}
122+
finally {
123+
_timer.Start();
124+
}
125+
}
126+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
using Nullinside.Api.Common.Twitch;
5+
6+
namespace TwitchStreamingTools.Services;
7+
8+
/// <summary>
9+
/// The contract for management credentials in the application.
10+
/// </summary>
11+
public interface IAccountManager {
12+
/// <summary>
13+
/// The current OAuth's twitch username.
14+
/// </summary>
15+
string? TwitchUsername { get; set; }
16+
17+
/// <summary>
18+
/// A flag indicating whether the credentials are currently valid.
19+
/// </summary>
20+
bool CredentialsAreValid { get; set; }
21+
22+
/// <summary>
23+
/// An event indicating that the current status of the credentials has changed.
24+
/// </summary>
25+
Action<bool>? OnCredentialsStatusChanged { get; set; }
26+
27+
/// <summary>
28+
/// An event indicating that the credentials have changed.
29+
/// </summary>
30+
Action<TwitchAccessToken?>? OnCredentialsChanged { get; set; }
31+
32+
/// <summary>
33+
/// Updates the credentials.
34+
/// </summary>
35+
/// <param name="bearer">The bearer token.</param>
36+
/// <param name="refresh">The refresh token.</param>
37+
/// <param name="expires">The <seealso cref="DateTime" /> when the credentials expire.</param>
38+
Task UpdateCredentials(string bearer, string refresh, DateTime expires);
39+
40+
/// <summary>
41+
/// Clears out the credentials.
42+
/// </summary>
43+
void DeleteCredentials();
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
using Nullinside.Api.Common.Twitch;
7+
8+
using TwitchStreamingTools.Utilities;
9+
using TwitchStreamingTools.ViewModels;
10+
using TwitchStreamingTools.ViewModels.Pages;
11+
12+
namespace TwitchStreamingTools.Services;
13+
14+
/// <summary>
15+
/// A wrapper that contains the registered services.
16+
/// </summary>
17+
public static class ServiceCollectionExtensions {
18+
/// <summary>
19+
/// Adds the services used throughout the application.
20+
/// </summary>
21+
/// <param name="collection">The services collection to initialize.</param>
22+
public static void AddCommonServices(this IServiceCollection collection) {
23+
// collection.AddSingleton<IRepository, Repository>();
24+
// collection.AddTransient<BusinessService>();
25+
collection.AddSingleton<IAccountManager, AccountManager>();
26+
collection.AddSingleton<ITwitchClientProxy, TwitchClientProxy>(_ => TwitchClientProxy.Instance);
27+
28+
collection.AddTransient<MainWindowViewModel>();
29+
collection.AddTransient<AccountViewModel>();
30+
collection.AddTransient<ChatViewModel>();
31+
collection.AddTransient<ITwitchApiProxy, TwitchApiWrapper>(TwitchApiWrapperFactory);
32+
}
33+
34+
/// <summary>
35+
/// A factory for generating twitch apis.
36+
/// </summary>
37+
/// <param name="_">not used.</param>
38+
/// <returns>A new instance of the <see cref="TwitchApiWrapper" /> class.</returns>
39+
private static TwitchApiWrapper TwitchApiWrapperFactory(IServiceProvider _) {
40+
Task<TwitchApiWrapper> task = TwitchApiWrapper.CreateApi();
41+
Task.WaitAll(task);
42+
return task.Result;
43+
}
44+
}

src/TwitchStreamingTools/TwitchStreamingTools.csproj

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@
2626
</ItemGroup>
2727

2828
<ItemGroup>
29-
<PackageReference Include="Avalonia" Version="11.2.8" />
30-
<PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
31-
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" />
32-
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.8" />
29+
<PackageReference Include="Avalonia" Version="11.3.0"/>
30+
<PackageReference Include="Avalonia.Desktop" Version="11.3.0"/>
31+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0"/>
32+
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.0"/>
3333
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
34-
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8" />
35-
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.8" />
36-
<PackageReference Include="Material.Avalonia" Version="3.10.2" />
34+
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8"/>
35+
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.0"/>
36+
<PackageReference Include="Material.Avalonia" Version="3.11.0"/>
37+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4"/>
3738
</ItemGroup>
3839

3940
<ItemGroup>

src/TwitchStreamingTools/ViewModels/MainWindowViewModel.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
using DynamicData;
88

9+
using Microsoft.Extensions.DependencyInjection;
10+
911
using ReactiveUI;
1012

1113
using TwitchStreamingTools.ViewModels.Pages;
@@ -18,6 +20,11 @@ namespace TwitchStreamingTools.ViewModels;
1820
/// The view model for the main UI.
1921
/// </summary>
2022
public class MainWindowViewModel : ViewModelBase {
23+
/// <summary>
24+
/// The dependency injection service provider.
25+
/// </summary>
26+
private readonly IServiceProvider _provider;
27+
2128
/// <summary>
2229
/// A flag indicating whether the menu is open.
2330
/// </summary>
@@ -26,7 +33,7 @@ public class MainWindowViewModel : ViewModelBase {
2633
/// <summary>
2734
/// The open page.
2835
/// </summary>
29-
private ViewModelBase _page = new AccountViewModel();
36+
private ViewModelBase _page;
3037

3138
/// <summary>
3239
/// The currently selected page.
@@ -36,7 +43,9 @@ public class MainWindowViewModel : ViewModelBase {
3643
/// <summary>
3744
/// Initializes a new instance of the <see cref="MainWindowViewModel" /> class.
3845
/// </summary>
39-
public MainWindowViewModel() {
46+
/// <param name="provider">The dependency injection service provider.</param>
47+
public MainWindowViewModel(IServiceProvider provider) {
48+
_provider = provider;
4049
OnToggleMenu = ReactiveCommand.Create(() => IsMenuOpen = !IsMenuOpen);
4150

4251
// Dynamically setup the pages
@@ -45,7 +54,7 @@ public MainWindowViewModel() {
4554
.SelectMany(a => a.GetTypes())
4655
.Where(t => (t.FullName?.StartsWith("TwitchStreamingTools.ViewModels.Pages") ?? false) &&
4756
typeof(PageViewModelBase).IsAssignableFrom(t) && t is { IsAbstract: false, IsInterface: false })
48-
.Select(t => new MenuItem(t, ((PageViewModelBase)Activator.CreateInstance(t)!).IconResourceKey))
57+
.Select(t => new MenuItem(t, (_provider.GetRequiredService(t) as PageViewModelBase)!.IconResourceKey))
4958
.ToList();
5059
MenuItems.AddRange(pages);
5160
_selectedMenuItem = pages.First(p => typeof(AccountViewModel).IsAssignableTo(p.ModelType));
@@ -54,6 +63,9 @@ public MainWindowViewModel() {
5463
OnSelectedMenuItemChanged();
5564
}
5665
};
66+
67+
// Set the initial page
68+
_page = (_provider.GetRequiredService(typeof(AccountViewModel)) as AccountViewModel)!;
5769
}
5870

5971
/// <summary>
@@ -94,7 +106,7 @@ public MenuItem SelectedMenuItem {
94106
/// Links the <see cref="Page" /> showing on the screen with changes to the <see cref="SelectedMenuItem" />.
95107
/// </summary>
96108
private void OnSelectedMenuItemChanged() {
97-
var viewModel = Activator.CreateInstance(SelectedMenuItem.ModelType) as ViewModelBase;
109+
var viewModel = _provider.GetService(SelectedMenuItem.ModelType) as ViewModelBase;
98110
if (null == viewModel) {
99111
return;
100112
}

0 commit comments

Comments
 (0)