Toolkit.Avalonia

This commit is contained in:
TheXamlGuy
2024-04-13 14:55:33 +01:00
parent 862e7b2e34
commit 705d84e56d
13 changed files with 763 additions and 3 deletions
+13
View File
@@ -0,0 +1,13 @@
using Avalonia.Threading;
using IDispatcher = Toolkit.Foundation.IDispatcher;
namespace Toolkit.Avalonia;
public class AvaloniaDispatcher :
IDispatcher
{
public async Task InvokeAsync(Action action)
{
await Dispatcher.UIThread.InvokeAsync(action);
}
}
@@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public class ClassicDesktopStyleApplicationHandler(INavigationContext navigationContext) :
INavigateHandler<IClassicDesktopStyleApplicationLifetime>
{
public Task Handle(Navigate<IClassicDesktopStyleApplicationLifetime> args,
CancellationToken cancellationToken = default)
{
if (Application.Current?.ApplicationLifetime is
IClassicDesktopStyleApplicationLifetime lifeTime)
{
if (args.Template is Window window)
{
lifeTime.MainWindow = window;
window.DataContext = args.Content;
navigationContext.Set(window);
}
}
return Task.CompletedTask;
}
}
+60
View File
@@ -0,0 +1,60 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public class ContentControlHandler(INavigationContext navigationContext) :
INavigateHandler<ContentControl>
{
public async Task Handle(Navigate<ContentControl> args,
CancellationToken cancellationToken)
{
if (args.Context is ContentControl contentControl)
{
if (args.Template is Control control)
{
TaskCompletionSource taskCompletionSource = new();
async void HandleLoaded(object? sender, RoutedEventArgs args)
{
control.Loaded -= HandleLoaded;
if (control.DataContext is object content)
{
if (content is IInitializer initializer)
{
await initializer.Initialize();
}
if (content is IActivated activated)
{
await activated.Activated();
}
}
taskCompletionSource.SetResult();
}
async void HandleUnloaded(object? sender, RoutedEventArgs args)
{
control.Unloaded -= HandleLoaded;
if (control.DataContext is object content)
{
if (content is IDeactivated deactivated)
{
await deactivated.Deactivated();
}
}
}
control.Loaded += HandleLoaded;
control.Unloaded += HandleUnloaded;
contentControl.Content = control;
contentControl.DataContext = args.Content;
navigationContext.Set(control);
await taskCompletionSource.Task;
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
using Toolkit.Foundation;
using Toolkit.UI.Controls.Avalonia;
namespace Toolkit.Avalonia;
public class ContentDialogHandler(IDispatcher dispatcher) :
INavigateHandler<ContentDialog>
{
public async Task Handle(Navigate<ContentDialog> args,
CancellationToken cancellationToken)
{
if (args.Context is ContentDialog contentDialog)
{
contentDialog.DataContext = args.Content;
async void HandlePrimaryButtonClick(FluentAvalonia.UI.Controls.ContentDialog sender,
FluentAvalonia.UI.Controls.ContentDialogButtonClickEventArgs args)
{
contentDialog.PrimaryButtonClick -= HandlePrimaryButtonClick;
if (contentDialog.DataContext is object content)
{
if (content is IPrimaryConfirmation primaryConfirmation)
{
if (!await primaryConfirmation.Confirm())
{
args.Cancel = true;
contentDialog.PrimaryButtonClick += HandlePrimaryButtonClick;
}
}
}
}
async void HandleSecondaryButtonClick(FluentAvalonia.UI.Controls.ContentDialog sender,
FluentAvalonia.UI.Controls.ContentDialogButtonClickEventArgs args)
{
contentDialog.SecondaryButtonClick -= HandleSecondaryButtonClick;
if (contentDialog.DataContext is object content)
{
if (content is ISecondaryConfirmation secondaryConfirmation)
{
if (!await secondaryConfirmation.Confirm())
{
args.Cancel = true;
contentDialog.SecondaryButtonClick += HandleSecondaryButtonClick;
}
}
}
}
async void HandleClosing(FluentAvalonia.UI.Controls.ContentDialog sender,
FluentAvalonia.UI.Controls.ContentDialogClosingEventArgs args)
{
if (args.Result == FluentAvalonia.UI.Controls.ContentDialogResult.Primary ||
args.Result == FluentAvalonia.UI.Controls.ContentDialogResult.Secondary)
{
contentDialog.Closing -= HandleClosing;
if (contentDialog.DataContext is object content)
{
if (content is IConfirmation confirmation)
{
if (!await confirmation.Confirm())
{
args.Cancel = true;
contentDialog.Closing += HandleClosing;
}
}
}
}
}
async void HandleOpened(FluentAvalonia.UI.Controls.ContentDialog sender,
EventArgs args)
{
contentDialog.Opened -= HandleOpened;
if (contentDialog.DataContext is object content)
{
if (content is IDeactivatable deactivatable)
{
async void DeactivateHandler(object? sender, EventArgs args)
{
deactivatable.DeactivateHandler -= DeactivateHandler;
await dispatcher.InvokeAsync(contentDialog.Hide);
}
deactivatable.DeactivateHandler += DeactivateHandler;
}
// A hack to wait for the dialog to finish loading up to make it appear more responsive
await Task.Delay(250, cancellationToken);
if (content is IInitializer initializer)
{
await initializer.Initialize();
}
if (content is IActivated activated)
{
await activated.Activated();
}
}
}
contentDialog.Opened += HandleOpened;
contentDialog.Closing += HandleClosing;
contentDialog.PrimaryButtonClick += HandlePrimaryButtonClick;
contentDialog.SecondaryButtonClick += HandleSecondaryButtonClick;
await contentDialog.ShowAsync();
contentDialog.PrimaryButtonClick += HandlePrimaryButtonClick;
contentDialog.SecondaryButtonClick += HandleSecondaryButtonClick;
}
}
}
+70
View File
@@ -0,0 +1,70 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Microsoft.Extensions.DependencyInjection;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public class ContentTemplate :
IContentTemplate,
IDataTemplate
{
public Control? Build(object? item)
{
if (item is IObservableViewModel observableViewModel)
{
if (observableViewModel.ServiceProvider is IServiceProvider provider)
{
IContentTemplateDescriptorProvider? contentTemplateProvider = provider.GetService<IContentTemplateDescriptorProvider>();
INavigationContext? viewModelContentBinder = provider.GetService<INavigationContext>();
if (contentTemplateProvider?.Get(item.GetType().Name) is IContentTemplateDescriptor descriptor)
{
if (provider.GetRequiredKeyedService(descriptor.TemplateType, descriptor.Key) is Control control)
{
async void HandleLoaded(object? sender, RoutedEventArgs args)
{
control.Loaded -= HandleLoaded;
if (control.DataContext is object content)
{
if (content is IInitializer initializer)
{
await initializer.Initialize();
}
if (content is IActivated activated)
{
await activated.Activated();
}
}
}
async void HandleUnloaded(object? sender, RoutedEventArgs args)
{
control.Unloaded -= HandleLoaded;
if (control.DataContext is object content)
{
if (content is IDeactivated deactivated)
{
await deactivated.Deactivated();
}
}
}
control.Loaded += HandleLoaded;
control.Unloaded += HandleUnloaded;
viewModelContentBinder?.Set(control);
return control;
}
}
}
}
return default;
}
public bool Match(object? data) => true;
}
+203
View File
@@ -0,0 +1,203 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using FluentAvalonia.UI.Navigation;
using System.Reflection;
using Toolkit.Foundation;
using Toolkit.UI.Controls.Avalonia;
namespace Toolkit.Avalonia;
public class FrameHandler(INavigationContext navigationContext) :
INavigateHandler<Frame>,
INavigateBackHandler<Frame>
{
public Task Handle(Navigate<Frame> args,
CancellationToken cancellationToken)
{
if (args.Context is Frame frame)
{
frame.NavigationPageFactory ??= new NavigationPageFactory();
if (args.Template is Control control)
{
void NavigatingFrom(object? sender,
Control control)
{
async void HandleNavigatingFrom(object? _,
NavigatingCancelEventArgs args)
{
Dictionary<string,object> results = [];
control.RemoveHandler(Frame.NavigatingFromEvent, HandleNavigatingFrom);
NavigatedFrom(sender, control, () => results);
if (control.DataContext is object content)
{
if (content is IPrimaryConfirmation confirmNavigation &&
!await confirmNavigation.Confirm())
{
args.Cancel = true;
}
if (!args.Cancel)
{
if (content is IDeactivating deactivating)
{
await deactivating.Deactivating();
}
Type contentType = content.GetType();
if (contentType.GetInterfaces() is Type[] contracts)
{
foreach (Type contract in contracts)
{
if (contract.Name == typeof(IDeactivating<>).Name &&
contract.GetGenericArguments() is { Length: 1 } arguments)
{
if (contentType.GetMethods().FirstOrDefault(x =>
x.Name == "Deactivating" && x.ReturnType == typeof(Task<>)
.MakeGenericType(arguments[0]))
is MethodInfo methodInfo)
{
if (methodInfo.GetCustomAttribute<NavigationContextAttribute>()
is NavigationContextAttribute attribute)
{
if (await methodInfo.InvokeAsync<object?>(content) is object result)
{
results.Add(attribute.Name, result);
}
}
}
}
}
}
}
}
}
control.AddHandler(Frame.NavigatingFromEvent, HandleNavigatingFrom);
}
void NavigatedFrom(object? sender,
Control control,
Func<Dictionary<string, object>> resultCallBack)
{
async void HandleNavigatedFrom(object? _,
NavigationEventArgs args)
{
control.RemoveHandler(Frame.NavigatedFromEvent, HandleNavigatedFrom);
if (args.NavigationMode == NavigationMode.New)
{
NavigatedTo(sender, control);
}
Dictionary<string, object> results = resultCallBack.Invoke();
async Task DoNavigatedFromAsync(object? content)
{
if (content is not null)
{
if (content is IDeactivated deactivated)
{
await deactivated.Deactivated();
}
Type contentType = content.GetType();
if (contentType.GetInterfaces() is Type[] contracts)
{
foreach (Type contract in contracts)
{
if (contract.Name == typeof(IActivated<>).Name &&
contract.GetGenericArguments() is { Length: 1 } arguments)
{
if (contentType.GetMethods().FirstOrDefault(x =>
x.Name == "NavigatedToAsync" &&
x.GetCustomAttribute<NavigationContextAttribute>()
is NavigationContextAttribute attribute && results.ContainsKey(attribute.Name))
is MethodInfo methodInfo)
{
if (methodInfo.GetCustomAttribute<NavigationContextAttribute>()
is NavigationContextAttribute attribute)
{
if (results.TryGetValue(attribute.Name, out object? value))
{
await methodInfo.InvokeAsync(content, value);
}
}
}
}
}
}
}
}
if (args.Source is TemplatedControl sourceTemplate)
{
if (sourceTemplate.DataContext is object content)
{
await DoNavigatedFromAsync(content);
}
}
if (sender is TemplatedControl senderTemplate)
{
if (senderTemplate.DataContext is object content)
{
await DoNavigatedFromAsync(content);
}
}
else
{
await DoNavigatedFromAsync(sender);
}
}
control.AddHandler(Frame.NavigatedFromEvent, HandleNavigatedFrom);
}
void NavigatedTo(object? sender,
Control control)
{
async void HandleNavigatedTo(object? _,
NavigationEventArgs __)
{
control.RemoveHandler(Frame.NavigatedToEvent, HandleNavigatedTo);
NavigatingFrom(sender, control);
if (control.DataContext is object content)
{
if (content is IInitializer initializer)
{
await initializer.Initialize();
}
if (content is IActivated activated)
{
await activated.Activated();
}
}
}
control.AddHandler(Frame.NavigatedToEvent, HandleNavigatedTo);
}
control.DataContext = args.Content;
navigationContext.Set(control);
NavigatedTo(args.Sender, control);
frame.NavigateFromObject(control);
}
}
return Task.CompletedTask;
}
public Task Handle(NavigateBack<Frame> args,
CancellationToken cancellationToken = default)
{
if (args.Context is Frame frame)
{
frame.GoBack();
}
return Task.CompletedTask;
}
}
+8
View File
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace Toolkit.Avalonia;
public interface INavigationContext
{
void Set(Control control);
}
@@ -0,0 +1,160 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddComponentConfigurationTemplate<TConfiguration, TValue, THeader,
TDescription, TAction>(this IServiceCollection services,
params object[]? parameters)
where TConfiguration : class
where THeader : class
where TDescription : class
where TAction : class
{
Type viewModelType = typeof(ComponentConfigurationViewModel<TConfiguration, TValue, THeader, TDescription, TAction>);
Type viewType = typeof(Button);
object key = viewModelType.Name.Replace("ViewModel", "");
services.AddTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, THeader, TDescription, TAction>>(provider =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, THeader, TDescription, TAction>>(parameters)!);
services.TryAddTransient(viewType);
services.AddKeyedTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, THeader, TDescription, TAction>>(key, (provider, key) =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, THeader, TDescription, TAction>>(parameters)!);
services.TryAddKeyedTransient(viewType, key);
services.AddTransient<IContentTemplateDescriptor>(provider =>
new ContentTemplateDescriptor(key, viewModelType, viewType, parameters));
services.TryAddTransient<THeader>();
services.TryAddTransient<TDescription>();
services.TryAddTransient<TAction>();
return services;
}
public static IServiceCollection AddComponentConfigurationTemplate<TConfiguration, TValue, TAction>(this IServiceCollection services,
Func<TConfiguration, TValue> valueDelegate,
object header,
object description,
params object[]? parameters)
where TConfiguration : class
where TAction : class
{
Type viewModelType = typeof(ComponentConfigurationViewModel<TConfiguration, TValue, TAction>);
Type viewType = typeof(Button);
object key = viewModelType.Name.Replace("ViewModel", "");
parameters = [valueDelegate, header, description, .. parameters ?? Enumerable.Empty<object?>()];
services.AddTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, TAction>>(provider =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, TAction>>(parameters)!);
services.TryAddTransient(viewType);
services.AddKeyedTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, TAction>>(key, (provider, key) =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, TAction>>(parameters)!);
services.TryAddKeyedTransient(viewType, key);
services.AddTransient<IContentTemplateDescriptor>(provider =>
new ContentTemplateDescriptor(key, viewModelType, viewType, parameters));
services.TryAddTransient<TAction>();
return services;
}
public static IServiceCollection AddComponentConfigurationTemplate<TConfiguration, TValue,
TDescription, TAction>(this IServiceCollection services,
Func<TConfiguration, TValue> valueDelegate,
object description,
params object[]? parameters)
where TConfiguration : class
where TDescription : class
where TAction : class
{
Type viewModelType = typeof(ComponentConfigurationViewModel<TConfiguration, TValue, TDescription, TAction>);
Type viewType = typeof(Button);
object key = viewModelType.Name.Replace("ViewModel", "");
parameters = [valueDelegate, description, .. parameters ?? Enumerable.Empty<object?>()];
services.AddTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, TDescription, TAction>>(provider =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, TDescription, TAction>>(parameters)!);
services.TryAddTransient(viewType);
services.AddKeyedTransient<IComponentConfigurationViewModel,
ComponentConfigurationViewModel<TConfiguration, TValue, TDescription, TAction>>(key, (provider, key) =>
provider.GetRequiredService<IServiceFactory>()
.Create<ComponentConfigurationViewModel<TConfiguration, TValue, TDescription, TAction>>(parameters)!);
services.TryAddKeyedTransient(viewType, key);
services.AddTransient<IContentTemplateDescriptor>(provider =>
new ContentTemplateDescriptor(key, viewModelType, viewType, parameters));
services.TryAddTransient<TDescription>();
services.TryAddTransient<TAction>();
return services;
}
public static IServiceCollection AddAvalonia(this IServiceCollection services)
{
services.AddTransient<IDispatcher, AvaloniaDispatcher>();
services.AddTransient<IContentTemplate, ContentTemplate>();
services.AddTransient<INavigationContext, NavigationContext>();
services.AddNavigateHandler<ClassicDesktopStyleApplicationHandler>();
services.AddNavigateHandler<SingleViewApplicationHandler>();
services.AddNavigateHandler<ContentControlHandler>();
services.AddNavigateHandler<FrameHandler>();
services.AddNavigateHandler<ContentDialogHandler>();
services.AddScoped<INavigationContextCollection, NavigationContextCollection>(provider => new NavigationContextCollection
{
{ typeof(IClassicDesktopStyleApplicationLifetime), typeof(IClassicDesktopStyleApplicationLifetime) },
{ typeof(ISingleViewApplicationLifetime), typeof(ISingleViewApplicationLifetime) }
});
services.AddTransient((Func<IServiceProvider, IProxyServiceCollection<IComponentBuilder>>)(provider =>
new ProxyServiceCollection<IComponentBuilder>(services =>
{
services.AddSingleton(provider.GetRequiredService<IDispatcher>());
services.AddTransient<IContentTemplateDescriptorProvider, ContentTemplateDescriptorProvider>();
services.AddTransient<IContentTemplate, ContentTemplate>();
services.AddTransient<INavigationContext, NavigationContext>();
services.AddNavigateHandler<ContentControlHandler>();
services.AddNavigateHandler<FrameHandler>();
services.AddNavigateHandler<ContentDialogHandler>();
})));
return services;
}
}
+38
View File
@@ -0,0 +1,38 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using HyperX.UI.Windows;
using System.Reflection;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public class NavigationContext(INavigationContextCollection contexts) :
INavigationContext
{
public void Set(Control control)
{
if (control.GetType().GetCustomAttributes<NavigationTargetAttribute>()
is IEnumerable<NavigationTargetAttribute> attributes)
{
foreach (NavigationTargetAttribute attribute in attributes)
{
if (!contexts.ContainsKey(attribute.Name))
{
if (control.Find<TemplatedControl>(attribute.Name) is TemplatedControl content)
{
contexts.Add(attribute.Name, content);
void HandleUnloaded(object? sender, RoutedEventArgs args)
{
control.Unloaded -= HandleUnloaded;
contexts.Remove(attribute.Name);
}
control.Unloaded += HandleUnloaded;
}
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
namespace Toolkit.Avalonia;
public class NavigationPageFactory :
INavigationPageFactory
{
public Control? GetPage(Type srcType)
{
return default;
}
public Control GetPageFromObject(object target)
{
return (Control)target;
}
}
@@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Toolkit.Foundation;
namespace Toolkit.Avalonia;
public class SingleViewApplicationHandler(INavigationContext navigationContext) :
INavigateHandler<ISingleViewApplicationLifetime>
{
public Task Handle(Navigate<ISingleViewApplicationLifetime> args,
CancellationToken cancellationToken = default)
{
if (Application.Current?.ApplicationLifetime is
ISingleViewApplicationLifetime lifeTime)
{
if (args.Template is Control control)
{
lifeTime.MainView = control;
control.DataContext = args.Content;
navigationContext.Set(control);
}
}
return Task.CompletedTask;
}
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0-beta1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Toolkit.Foundation\Toolkit.Foundation.csproj" />
<ProjectReference Include="..\Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj" />
<ProjectReference Include="..\Toolkit.UI.Controls.Avalonia\Toolkit.UI.Controls.Avalonia.csproj" />
</ItemGroup>
</Project>
+9 -3
View File
@@ -3,11 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Foundation", "Toolkit.Foundation\Toolkit.Foundation.csproj", "{66968F8D-689E-49D8-9370-DFF099C56202}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit.Foundation", "Toolkit.Foundation\Toolkit.Foundation.csproj", "{66968F8D-689E-49D8-9370-DFF099C56202}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Avalonia", "Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj", "{E091FA94-2F15-403A-98D1-4557C2FF9A02}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit.UI.Avalonia", "Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj", "{E091FA94-2F15-403A-98D1-4557C2FF9A02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Controls.Avalonia", "Toolkit.UI.Controls.Avalonia\Toolkit.UI.Controls.Avalonia.csproj", "{8841990D-A246-495D-9A40-C39BA4347505}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit.UI.Controls.Avalonia", "Toolkit.UI.Controls.Avalonia\Toolkit.UI.Controls.Avalonia.csproj", "{8841990D-A246-495D-9A40-C39BA4347505}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Avalonia", "Toolkit.Avalonia\Toolkit.Avalonia.csproj", "{9585A317-4405-4E39-BDBE-23EF822FBF34}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,6 +29,10 @@ Global
{8841990D-A246-495D-9A40-C39BA4347505}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Release|Any CPU.Build.0 = Release|Any CPU
{9585A317-4405-4E39-BDBE-23EF822FBF34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9585A317-4405-4E39-BDBE-23EF822FBF34}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9585A317-4405-4E39-BDBE-23EF822FBF34}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9585A317-4405-4E39-BDBE-23EF822FBF34}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE