diff --git a/Toolkit.Avalonia/AvaloniaDispatcher.cs b/Toolkit.Avalonia/AvaloniaDispatcher.cs new file mode 100644 index 0000000..0870623 --- /dev/null +++ b/Toolkit.Avalonia/AvaloniaDispatcher.cs @@ -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); + } +} \ No newline at end of file diff --git a/Toolkit.Avalonia/ClassicDesktopStyleApplicationHandler.cs b/Toolkit.Avalonia/ClassicDesktopStyleApplicationHandler.cs new file mode 100644 index 0000000..42d67c0 --- /dev/null +++ b/Toolkit.Avalonia/ClassicDesktopStyleApplicationHandler.cs @@ -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 +{ + public Task Handle(Navigate 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; + } +} \ No newline at end of file diff --git a/Toolkit.Avalonia/ContentControlHandler.cs b/Toolkit.Avalonia/ContentControlHandler.cs new file mode 100644 index 0000000..ccef36e --- /dev/null +++ b/Toolkit.Avalonia/ContentControlHandler.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Toolkit.Foundation; + +namespace Toolkit.Avalonia; + +public class ContentControlHandler(INavigationContext navigationContext) : + INavigateHandler +{ + public async Task Handle(Navigate 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; + } + } + } +} diff --git a/Toolkit.Avalonia/ContentDialogHandler.cs b/Toolkit.Avalonia/ContentDialogHandler.cs new file mode 100644 index 0000000..33394ae --- /dev/null +++ b/Toolkit.Avalonia/ContentDialogHandler.cs @@ -0,0 +1,113 @@ +using Toolkit.Foundation; +using Toolkit.UI.Controls.Avalonia; + +namespace Toolkit.Avalonia; + +public class ContentDialogHandler(IDispatcher dispatcher) : + INavigateHandler +{ + public async Task Handle(Navigate 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; + } + } +} \ No newline at end of file diff --git a/Toolkit.Avalonia/ContentTemplate.cs b/Toolkit.Avalonia/ContentTemplate.cs new file mode 100644 index 0000000..86a7144 --- /dev/null +++ b/Toolkit.Avalonia/ContentTemplate.cs @@ -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(); + INavigationContext? viewModelContentBinder = provider.GetService(); + + 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; +} diff --git a/Toolkit.Avalonia/FrameHandler.cs b/Toolkit.Avalonia/FrameHandler.cs new file mode 100644 index 0000000..fa5a17d --- /dev/null +++ b/Toolkit.Avalonia/FrameHandler.cs @@ -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, + INavigateBackHandler +{ + public Task Handle(Navigate 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 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() + is NavigationContextAttribute attribute) + { + if (await methodInfo.InvokeAsync(content) is object result) + { + results.Add(attribute.Name, result); + } + } + } + } + } + } + } + } + } + + control.AddHandler(Frame.NavigatingFromEvent, HandleNavigatingFrom); + } + + void NavigatedFrom(object? sender, + Control control, + Func> resultCallBack) + { + async void HandleNavigatedFrom(object? _, + NavigationEventArgs args) + { + control.RemoveHandler(Frame.NavigatedFromEvent, HandleNavigatedFrom); + if (args.NavigationMode == NavigationMode.New) + { + NavigatedTo(sender, control); + } + + Dictionary 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() + is NavigationContextAttribute attribute && results.ContainsKey(attribute.Name)) + is MethodInfo methodInfo) + { + if (methodInfo.GetCustomAttribute() + 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 args, + CancellationToken cancellationToken = default) + { + if (args.Context is Frame frame) + { + frame.GoBack(); + } + + return Task.CompletedTask; + } +} diff --git a/Toolkit.Avalonia/INavigationContext.cs b/Toolkit.Avalonia/INavigationContext.cs new file mode 100644 index 0000000..53d3792 --- /dev/null +++ b/Toolkit.Avalonia/INavigationContext.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls; + +namespace Toolkit.Avalonia; + +public interface INavigationContext +{ + void Set(Control control); +} \ No newline at end of file diff --git a/Toolkit.Avalonia/IServiceCollectionExtensions.cs b/Toolkit.Avalonia/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..f1338cf --- /dev/null +++ b/Toolkit.Avalonia/IServiceCollectionExtensions.cs @@ -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(this IServiceCollection services, + params object[]? parameters) + where TConfiguration : class + where THeader : class + where TDescription : class + where TAction : class + { + Type viewModelType = typeof(ComponentConfigurationViewModel); + Type viewType = typeof(Button); + + object key = viewModelType.Name.Replace("ViewModel", ""); + + services.AddTransient>(provider => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddTransient(viewType); + + services.AddKeyedTransient>(key, (provider, key) => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddKeyedTransient(viewType, key); + + services.AddTransient(provider => + new ContentTemplateDescriptor(key, viewModelType, viewType, parameters)); + + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddTransient(); + + return services; + } + + public static IServiceCollection AddComponentConfigurationTemplate(this IServiceCollection services, + Func valueDelegate, + object header, + object description, + params object[]? parameters) + where TConfiguration : class + where TAction : class + { + Type viewModelType = typeof(ComponentConfigurationViewModel); + Type viewType = typeof(Button); + + object key = viewModelType.Name.Replace("ViewModel", ""); + + parameters = [valueDelegate, header, description, .. parameters ?? Enumerable.Empty()]; + + services.AddTransient>(provider => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddTransient(viewType); + + services.AddKeyedTransient>(key, (provider, key) => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddKeyedTransient(viewType, key); + + services.AddTransient(provider => + new ContentTemplateDescriptor(key, viewModelType, viewType, parameters)); + + services.TryAddTransient(); + + return services; + } + + public static IServiceCollection AddComponentConfigurationTemplate(this IServiceCollection services, + Func valueDelegate, + object description, + params object[]? parameters) + where TConfiguration : class + where TDescription : class + where TAction : class + { + Type viewModelType = typeof(ComponentConfigurationViewModel); + Type viewType = typeof(Button); + + object key = viewModelType.Name.Replace("ViewModel", ""); + + parameters = [valueDelegate, description, .. parameters ?? Enumerable.Empty()]; + + services.AddTransient>(provider => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddTransient(viewType); + + services.AddKeyedTransient>(key, (provider, key) => + provider.GetRequiredService() + .Create>(parameters)!); + + services.TryAddKeyedTransient(viewType, key); + + services.AddTransient(provider => + new ContentTemplateDescriptor(key, viewModelType, viewType, parameters)); + + services.TryAddTransient(); + services.TryAddTransient(); + + return services; + } + + public static IServiceCollection AddAvalonia(this IServiceCollection services) + { + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddNavigateHandler(); + services.AddNavigateHandler(); + services.AddNavigateHandler(); + services.AddNavigateHandler(); + services.AddNavigateHandler(); + + services.AddScoped(provider => new NavigationContextCollection + { + { typeof(IClassicDesktopStyleApplicationLifetime), typeof(IClassicDesktopStyleApplicationLifetime) }, + { typeof(ISingleViewApplicationLifetime), typeof(ISingleViewApplicationLifetime) } + }); + + services.AddTransient((Func>)(provider => + new ProxyServiceCollection(services => + { + services.AddSingleton(provider.GetRequiredService()); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + + services.AddNavigateHandler(); + services.AddNavigateHandler(); + services.AddNavigateHandler(); + }))); + + return services; + } +} \ No newline at end of file diff --git a/Toolkit.Avalonia/NavigationContext.cs b/Toolkit.Avalonia/NavigationContext.cs new file mode 100644 index 0000000..538cc92 --- /dev/null +++ b/Toolkit.Avalonia/NavigationContext.cs @@ -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() + is IEnumerable attributes) + { + foreach (NavigationTargetAttribute attribute in attributes) + { + if (!contexts.ContainsKey(attribute.Name)) + { + if (control.Find(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; + } + } + } + } + } +} + diff --git a/Toolkit.Avalonia/NavigationPageFactory.cs b/Toolkit.Avalonia/NavigationPageFactory.cs new file mode 100644 index 0000000..511d563 --- /dev/null +++ b/Toolkit.Avalonia/NavigationPageFactory.cs @@ -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; + } +} diff --git a/Toolkit.Avalonia/SingleViewApplicationHandler.cs b/Toolkit.Avalonia/SingleViewApplicationHandler.cs new file mode 100644 index 0000000..530bade --- /dev/null +++ b/Toolkit.Avalonia/SingleViewApplicationHandler.cs @@ -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 +{ + public Task Handle(Navigate 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; + } +} diff --git a/Toolkit.Avalonia/Toolkit.Avalonia.csproj b/Toolkit.Avalonia/Toolkit.Avalonia.csproj new file mode 100644 index 0000000..2670c0f --- /dev/null +++ b/Toolkit.Avalonia/Toolkit.Avalonia.csproj @@ -0,0 +1,15 @@ + + + net8.0 + enable + enable + + + + + + + + + + \ No newline at end of file diff --git a/Toolkit.sln b/Toolkit.sln index 8c89dac..3afc236 100644 --- a/Toolkit.sln +++ b/Toolkit.sln @@ -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