diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..64bfbb6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +ko_fi: canton7 +custom: ['https://www.paypal.com/donate?hosted_button_id=92FADFBYS42MU'] diff --git a/.github/ISSUE_TEMPLATE/bug-template.md b/.github/ISSUE_TEMPLATE/bug-template.md new file mode 100644 index 0000000..c61a5be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-template.md @@ -0,0 +1,17 @@ +--- +name: Report a bug +about: If you've definitely found something wrong, use this. Not sure? Open a discussion. +--- + +**Description** +A clear and concise description of what the bug is. Use screenshots as necessary. + +**To Reproduce** +Code to reproduce the bug, which someone else can run. + +**Version Info** + - Stylet version: [e.g. 1.2.3] + - Runtime version: [e.g. 5.0.300] + +**Additional Info** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..2a085ad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Open a Discussion + url: https://github.com/canton7/stylet/discussions/new + about: If you've got a question, suggestion, or something you're not sure about, please open a discussion. \ No newline at end of file diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md new file mode 100644 index 0000000..98b5119 --- /dev/null +++ b/.github/pull-request-template.md @@ -0,0 +1,9 @@ +**Checklist** + +Thanks for contributing! Before we start, there are a few things we need to check: + +1. This Pull Request has a corresponding Issue. +2. You've discussed your intention to work on this feature/bug fix. +3. This feature branch is based on develop (**not** master). The bar above should say "base: develop". + +Thanks! \ No newline at end of file diff --git a/Bootstrappers/AutofacBootstrapper.cs b/Bootstrappers/AutofacBootstrapper.cs index 4bddc08..ba54b74 100644 --- a/Bootstrappers/AutofacBootstrapper.cs +++ b/Bootstrappers/AutofacBootstrapper.cs @@ -12,10 +12,10 @@ namespace Bootstrappers { private IContainer container; - private object _rootViewModel; - protected virtual object RootViewModel + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel { - get { return this._rootViewModel ?? (this._rootViewModel = this.GetInstance(typeof(TRootViewModel))); } + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } } protected override void ConfigureBootstrapper() @@ -43,7 +43,9 @@ namespace Bootstrappers builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().ExternallyOwned(); // Not singleton! - builder.RegisterAssemblyTypes(this.GetType().Assembly).ExternallyOwned(); + + // See https://github.com/canton7/Stylet/discussions/211 + builder.RegisterAssemblyTypes(this.GetType().Assembly).Where(x => !x.Name.Contains("ProcessedByFody")).ExternallyOwned(); } /// diff --git a/Bootstrappers/Bootstrappers.csproj b/Bootstrappers/Bootstrappers.csproj index 1ccd9ff..dad8bd6 100644 --- a/Bootstrappers/Bootstrappers.csproj +++ b/Bootstrappers/Bootstrappers.csproj @@ -9,8 +9,9 @@ Properties Bootstrappers Bootstrappers - v4.5 + v4.7.2 512 + true @@ -30,42 +31,7 @@ 4 - - packages\Autofac.4.2.1\lib\net45\Autofac.dll - True - - - packages\Castle.Core.3.3.3\lib\net45\Castle.Core.dll - True - - - packages\Castle.Windsor.3.3.0\lib\net45\Castle.Windsor.dll - - - packages\Unity.3.5.1404.0\lib\net45\Microsoft.Practices.Unity.dll - - - packages\Unity.3.5.1404.0\lib\net45\Microsoft.Practices.Unity.Configuration.dll - - - packages\Unity.3.5.1404.0\lib\net45\Microsoft.Practices.Unity.RegistrationByConvention.dll - - - packages\Ninject.3.2.2.0\lib\net45-full\Ninject.dll - - - packages\NUnit.3.5.0\lib\net45\nunit.framework.dll - True - - - packages\structuremap.3.1.6.186\lib\net40\StructureMap.dll - True - - - packages\structuremap.3.1.6.186\lib\net40\StructureMap.Net4.dll - True - @@ -80,20 +46,19 @@ + + - - - diff --git a/Bootstrappers/CastleWindsorBootstrapper.cs b/Bootstrappers/CastleWindsorBootstrapper.cs index fe2a926..c9940ff 100644 --- a/Bootstrappers/CastleWindsorBootstrapper.cs +++ b/Bootstrappers/CastleWindsorBootstrapper.cs @@ -14,10 +14,10 @@ namespace Bootstrappers { private IWindsorContainer container; - private object _rootViewModel; - protected virtual object RootViewModel + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel { - get { return this._rootViewModel ?? (this._rootViewModel = this.GetInstance(typeof(TRootViewModel))); } + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } } protected override void ConfigureBootstrapper() @@ -39,7 +39,7 @@ namespace Bootstrappers }; // Stylet does its own disposal of ViewModels: Castle Windsor shouldn't be doing the same - // Castle Windsor seems to be ver opinionated on this point, insisting that the container + // Castle Windsor seems to be very opinionated on this point, insisting that the container // should be responsible for disposing all components. This is at odds with Stylet's approach // (and indeed common sense). #pragma warning disable CS0618 // Type or member is obsolete diff --git a/Bootstrappers/MicrosoftDependencyInjectionBootstrapper.cs b/Bootstrappers/MicrosoftDependencyInjectionBootstrapper.cs new file mode 100644 index 0000000..43caaf1 --- /dev/null +++ b/Bootstrappers/MicrosoftDependencyInjectionBootstrapper.cs @@ -0,0 +1,79 @@ +using Autofac; +using Microsoft.Extensions.DependencyInjection; +using Stylet; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Bootstrappers +{ + public class MicrosoftDependencyInjectionBootstrapper : BootstrapperBase where TRootViewModel : class + { + private ServiceProvider serviceProvider; + + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel + { + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } + } + + public IServiceProvider ServiceProvider + { + get { return this.serviceProvider; } + } + + protected override void ConfigureBootstrapper() + { + var services = new ServiceCollection(); + this.DefaultConfigureIoC(services); + this.ConfigureIoC(services); + this.serviceProvider = services.BuildServiceProvider(); + } + + /// + /// Carries out default configuration of the IoC container. Override if you don't want to do this + /// + protected virtual void DefaultConfigureIoC(IServiceCollection services) + { + var viewManagerConfig = new ViewManagerConfig() + { + ViewFactory = this.GetInstance, + ViewAssemblies = new List() { this.GetType().Assembly } + }; + + services.AddSingleton(new ViewManager(viewManagerConfig)); + services.AddTransient(); + + services.AddSingleton(this); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); // Not singleton! + // Also need a factory + services.AddSingleton>(() => new MessageBoxViewModel()); + } + + /// + /// Override to add your own types to the IoC container. + /// + protected virtual void ConfigureIoC(IServiceCollection services) { } + + public override object GetInstance(Type type) + { + return this.serviceProvider.GetRequiredService(type); + } + + protected override void Launch() + { + base.DisplayRootView(this.RootViewModel); + } + + public override void Dispose() + { + base.Dispose(); + + ScreenExtensions.TryDispose(this._rootViewModel); + if (this.serviceProvider != null) + this.serviceProvider.Dispose(); + } + } +} diff --git a/Bootstrappers/NinjectBootstrapper.cs b/Bootstrappers/NinjectBootstrapper.cs index 092bc54..cfce839 100644 --- a/Bootstrappers/NinjectBootstrapper.cs +++ b/Bootstrappers/NinjectBootstrapper.cs @@ -11,10 +11,10 @@ namespace Bootstrappers { private IKernel kernel; - private object _rootViewModel; - protected virtual object RootViewModel + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel { - get { return this._rootViewModel ?? (this._rootViewModel = this.GetInstance(typeof(TRootViewModel))); } + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } } protected override void ConfigureBootstrapper() diff --git a/Bootstrappers/StructureMapBootstrapper.cs b/Bootstrappers/StructureMapBootstrapper.cs index 9845ae9..5d0233c 100644 --- a/Bootstrappers/StructureMapBootstrapper.cs +++ b/Bootstrappers/StructureMapBootstrapper.cs @@ -12,10 +12,10 @@ namespace Bootstrappers { private IContainer container; - private object _rootViewModel; - protected virtual object RootViewModel + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel { - get { return this._rootViewModel ?? (this._rootViewModel = this.GetInstance(typeof(TRootViewModel))); } + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } } protected override void ConfigureBootstrapper() diff --git a/Bootstrappers/Tests/BootstrapperTests.cs b/Bootstrappers/Tests/BootstrapperTests.cs index 0610d2f..4a04a6b 100644 --- a/Bootstrappers/Tests/BootstrapperTests.cs +++ b/Bootstrappers/Tests/BootstrapperTests.cs @@ -159,6 +159,9 @@ namespace Bootstrappers.Tests [Test] public void DoesNotDisposeTransientInstances() { + if (!this.Autobinds) + Assert.Ignore("Autobinding not supported"); + StubType.Reset(); var vm = this.bootstrapper.GetInstance(typeof(StubType)); diff --git a/Bootstrappers/Tests/MicrosoftDependencyInjectionTests.cs b/Bootstrappers/Tests/MicrosoftDependencyInjectionTests.cs new file mode 100644 index 0000000..0581131 --- /dev/null +++ b/Bootstrappers/Tests/MicrosoftDependencyInjectionTests.cs @@ -0,0 +1,71 @@ +using Autofac; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bootstrappers.Tests +{ + public class MyMicrosoftDependencyInjectionBootstrapper : MicrosoftDependencyInjectionBootstrapper, ITestBootstrapper + { + public List ConfigureLog { get; set; } + + public int DisposeCount { get; private set; } + + public MyMicrosoftDependencyInjectionBootstrapper() + { + this.ConfigureLog = new List(); + } + + protected override void Configure() + { + base.Configure(); + this.ConfigureLog.Add("Configure"); + } + + protected override void DefaultConfigureIoC(IServiceCollection services) + { + base.DefaultConfigureIoC(services); + this.ConfigureLog.Add("DefaultConfigureIoC"); + } + + protected override void ConfigureIoC(IServiceCollection services) + { + base.ConfigureIoC(services); + this.ConfigureLog.Add("ConfigureIoC"); + } + + public new object GetInstance(Type type) + { + return base.GetInstance(type); + } + + public new void ConfigureBootstrapper() + { + base.ConfigureBootstrapper(); + } + + public override void Dispose() + { + base.Dispose(); + this.DisposeCount++; + } + } + + [TestFixture(Category = "ServiceCollection")] + public class MicrosoftDependencyInjectionTests : BootstrapperTests + { + public MicrosoftDependencyInjectionTests() + { + this.Autobinds = false; + } + + public override MyMicrosoftDependencyInjectionBootstrapper CreateBootstrapper() + { + return new MyMicrosoftDependencyInjectionBootstrapper(); + } + } +} diff --git a/Bootstrappers/UnityBootstrapper.cs b/Bootstrappers/UnityBootstrapper.cs index f204055..77ccf4d 100644 --- a/Bootstrappers/UnityBootstrapper.cs +++ b/Bootstrappers/UnityBootstrapper.cs @@ -11,10 +11,10 @@ namespace Bootstrappers { private IUnityContainer container; - private object _rootViewModel; - protected virtual object RootViewModel + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel { - get { return this._rootViewModel ?? (this._rootViewModel = this.GetInstance(typeof(TRootViewModel))); } + get { return this._rootViewModel ?? (this._rootViewModel = (TRootViewModel)this.GetInstance(typeof(TRootViewModel))); } } protected override void ConfigureBootstrapper() diff --git a/Bootstrappers/packages.config b/Bootstrappers/packages.config deleted file mode 100644 index 8b5cd13..0000000 --- a/Bootstrappers/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2cadc74..c3ea7cf 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,20 @@ Stylet Changelog ================ +v1.3.6 +------ + + - If an Action returns a Task, await it in an `async void` method (#53) + - Allow Actions to have explicit targets (`{s:Action ..., Target=...}`) (#177) + - Allow Actions to invoke static methods (#177) + - Actions: if the target does not implement `INotifyPropertyChanged`, still try and evaluate guard properties (#214) + - Add an extra layer to the bootstrapper class hierarchy between `BootstrapperBase` and `Bootstrapper`, + for people who don't have a root ViewModel + - Allow starting the Bootstrapper without an Application (#206) + - Expose SynchronousDispatcher and ApplicationDispatcher to assign to `Execute.Dispatcher` (#217) + - Improve the sample bootstrappers (fix Autofac, add Microsoft.Services.DependencyInjection) + - Improve samples + v1.3.5 ------ diff --git a/Rakefile b/Rakefile index f0dc92c..6f305ad 100644 --- a/Rakefile +++ b/Rakefile @@ -19,21 +19,20 @@ directory COVERAGE_DIR desc "Build the project using the current CONFIG (or Debug)" task :build do - # https://github.com/novotnyllc/MSBuildSdkExtras/pull/249 - sh 'dotnet', 'build', '-c', CONFIG, CSPROJ, '/nowarn:MSB4011' + sh 'dotnet', 'build', '-c', CONFIG, CSPROJ end desc "Run unit tests using the current CONFIG (or Debug)" task :test do - sh 'dotnet', 'test', '-c', CONFIG, UNIT_TESTS, '/nowarn:MSB4011' + sh 'dotnet', 'test', '-c', CONFIG, UNIT_TESTS end desc "Create NuGet package" task :package do # Not sure why these have to be this way around, but they do - sh 'dotnet', 'pack', '--no-build', '-c', CONFIG, CSPROJ, "-p:NuSpecFile=../#{NUSPEC_START}", '/nowarn:MSB4011' - sh 'dotnet', 'pack', '--no-build', '-c', CONFIG, CSPROJ, '-p:IncludeSymbols=true', '/nowarn:MSB4011' - sh 'dotnet', 'pack', '-c', CONFIG, TEMPLATES_CSPROJ, '/nowarn:MSB4011' + sh 'dotnet', 'pack', '--no-build', '-c', CONFIG, CSPROJ, "-p:NuSpecFile=../#{NUSPEC_START}" + sh 'dotnet', 'pack', '--no-build', '-c', CONFIG, CSPROJ, '-p:IncludeSymbols=true' + sh 'dotnet', 'pack', '-c', CONFIG, TEMPLATES_CSPROJ end desc "Bump version number" diff --git a/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml b/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml index 0db8334..cdaffa3 100644 --- a/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml +++ b/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml @@ -10,7 +10,7 @@ - + diff --git a/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs b/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs index c739a2c..c5854ce 100644 --- a/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs +++ b/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs @@ -15,5 +15,10 @@ namespace Stylet.Samples.HelloDialog { this.RequestClose(null); } + + public void Save() + { + this.RequestClose(true); + } } } diff --git a/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs b/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs index 109f128..1ae3cfe 100644 --- a/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs +++ b/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs @@ -24,8 +24,9 @@ namespace Stylet.Samples.HelloDialog this.NameString = "Click the button to show the dialog"; } - public void ShowDialog() + public async System.Threading.Tasks.Task ShowDialog() { + throw new Exception("KABLAMMO"); var dialogVm = this.dialogFactory.CreateDialog1(); var result = this.windowManager.ShowDialog(dialogVm); if (result.GetValueOrDefault()) diff --git a/Samples/Stylet.Samples.NavigationController/App.config b/Samples/Stylet.Samples.NavigationController/App.config new file mode 100644 index 0000000..5754728 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.NavigationController/App.xaml b/Samples/Stylet.Samples.NavigationController/App.xaml new file mode 100644 index 0000000..63055a5 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.NavigationController/App.xaml.cs b/Samples/Stylet.Samples.NavigationController/App.xaml.cs new file mode 100644 index 0000000..b674f19 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet.Samples.NavigationController +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Bootstrapper.cs b/Samples/Stylet.Samples.NavigationController/Bootstrapper.cs new file mode 100644 index 0000000..8429522 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Bootstrapper.cs @@ -0,0 +1,27 @@ +using System; +using Stylet; +using StyletIoC; +using Stylet.Samples.NavigationController.Pages; + +namespace Stylet.Samples.NavigationController +{ + public class Bootstrapper : Bootstrapper + { + protected override void ConfigureIoC(IStyletIoCBuilder builder) + { + builder.Bind().And().To().InSingletonScope(); + // https://github.com/canton7/Stylet/issues/24 + builder.Bind>().ToFactory>(c => () => c.Get()); + builder.Bind>().ToFactory>(c => () => c.Get()); + } + + protected override void OnLaunch() + { + // There's a circular dependency, where ShellViewModel -> HeaderViewModel -> NavigationController -> ShellViewModel + // We break this by assigning the ShellViewModel to the NavigationController after constructing it + var navigationController = this.Container.Get(); + navigationController.Delegate = this.RootViewModel; + navigationController.NavigateToPage1(); + } + } +} diff --git a/Samples/Stylet.Samples.NavigationController/NavigationController.cs b/Samples/Stylet.Samples.NavigationController/NavigationController.cs new file mode 100644 index 0000000..395737b --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/NavigationController.cs @@ -0,0 +1,46 @@ +using Stylet.Samples.NavigationController.Pages; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.NavigationController +{ + public interface INavigationController + { + void NavigateToPage1(); + void NavigateToPage2(string initiator); + } + + public interface INavigationControllerDelegate + { + void NavigateTo(IScreen screen); + } + + public class NavigationController : INavigationController + { + private readonly Func page1ViewModelFactory; + private readonly Func page2ViewModelFactory; + + public INavigationControllerDelegate Delegate { get; set; } + + public NavigationController(Func page1ViewModelFactory, Func page2ViewModelFactory) + { + this.page1ViewModelFactory = page1ViewModelFactory ?? throw new ArgumentNullException(nameof(page1ViewModelFactory)); + this.page2ViewModelFactory = page2ViewModelFactory ?? throw new ArgumentNullException(nameof(page2ViewModelFactory)); + } + + public void NavigateToPage1() + { + this.Delegate?.NavigateTo(this.page1ViewModelFactory()); + } + + public void NavigateToPage2(string initiator) + { + var vm = this.page2ViewModelFactory(); + vm.Initiator = initiator; + this.Delegate?.NavigateTo(vm); + } + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Pages/HeaderView.xaml b/Samples/Stylet.Samples.NavigationController/Pages/HeaderView.xaml new file mode 100644 index 0000000..69b7b10 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/HeaderView.xaml @@ -0,0 +1,15 @@ + + + Go to: + + + + diff --git a/Samples/Stylet.Samples.NavigationController/Pages/HeaderViewModel.cs b/Samples/Stylet.Samples.NavigationController/Pages/HeaderViewModel.cs new file mode 100644 index 0000000..69c692d --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/HeaderViewModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.NavigationController.Pages +{ + public class HeaderViewModel : Screen + { + private readonly INavigationController navigationController; + + public HeaderViewModel(INavigationController navigationController) + { + this.navigationController = navigationController ?? throw new ArgumentNullException(nameof(navigationController)); + } + + public void NavigateToPage1() => this.navigationController.NavigateToPage1(); + public void NavigateToPage2() => this.navigationController.NavigateToPage2("the Header"); + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Pages/Page1View.xaml b/Samples/Stylet.Samples.NavigationController/Pages/Page1View.xaml new file mode 100644 index 0000000..8a4ca49 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/Page1View.xaml @@ -0,0 +1,16 @@ + + + + This is page 1 + + + + diff --git a/Samples/Stylet.Samples.NavigationController/Pages/Page1ViewModel.cs b/Samples/Stylet.Samples.NavigationController/Pages/Page1ViewModel.cs new file mode 100644 index 0000000..16a864b --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/Page1ViewModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.NavigationController.Pages +{ + public class Page1ViewModel : Screen + { + private readonly INavigationController navigationController; + + public Page1ViewModel(INavigationController navigationController) + { + this.navigationController = navigationController ?? throw new ArgumentNullException(nameof(navigationController)); + } + + public void NavigateToPage2() => this.navigationController.NavigateToPage2("Page 1"); + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Pages/Page2View.xaml b/Samples/Stylet.Samples.NavigationController/Pages/Page2View.xaml new file mode 100644 index 0000000..809a0f3 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/Page2View.xaml @@ -0,0 +1,17 @@ + + + + This is page 2 + + + + + diff --git a/Samples/Stylet.Samples.NavigationController/Pages/Page2ViewModel.cs b/Samples/Stylet.Samples.NavigationController/Pages/Page2ViewModel.cs new file mode 100644 index 0000000..97ded94 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/Page2ViewModel.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.NavigationController.Pages +{ + public class Page2ViewModel : Screen + { + private readonly INavigationController navigationController; + + private string _initiator; + public string Initiator + { + get => this._initiator; + set => this.SetAndNotify(ref this._initiator, value); + } + + public Page2ViewModel(INavigationController navigationController) + { + this.navigationController = navigationController ?? throw new ArgumentNullException(nameof(navigationController)); + } + + public void NavigateToPage1() => this.navigationController.NavigateToPage1(); + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Pages/ShellView.xaml b/Samples/Stylet.Samples.NavigationController/Pages/ShellView.xaml new file mode 100644 index 0000000..20c586e --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/ShellView.xaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/Samples/Stylet.Samples.NavigationController/Pages/ShellViewModel.cs b/Samples/Stylet.Samples.NavigationController/Pages/ShellViewModel.cs new file mode 100644 index 0000000..62dc680 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Pages/ShellViewModel.cs @@ -0,0 +1,20 @@ +using System; +using Stylet; + +namespace Stylet.Samples.NavigationController.Pages +{ + public class ShellViewModel : Conductor, INavigationControllerDelegate + { + public HeaderViewModel HeaderViewModel { get; } + + public ShellViewModel(HeaderViewModel headerViewModel) + { + this.HeaderViewModel = headerViewModel ?? throw new ArgumentNullException(nameof(headerViewModel)); + } + + public void NavigateTo(IScreen screen) + { + this.ActivateItem(screen); + } + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.NavigationController/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e3b736d --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Stylet.Samples.NavigationController")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.NavigationController")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Samples/Stylet.Samples.NavigationController/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.NavigationController/Properties/Resources.Designer.cs new file mode 100644 index 0000000..9596f8d --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Properties/Resources.Designer.cs @@ -0,0 +1,70 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +namespace Stylet.Samples.NavigationController.Properties +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stylet.Samples.NavigationController.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Properties/Resources.resx b/Samples/Stylet.Samples.NavigationController/Properties/Resources.resx new file mode 100644 index 0000000..ffecec8 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.NavigationController/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.NavigationController/Properties/Settings.Designer.cs new file mode 100644 index 0000000..887775d --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Properties/Settings.Designer.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +namespace Stylet.Samples.NavigationController.Properties +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/Samples/Stylet.Samples.NavigationController/Properties/Settings.settings b/Samples/Stylet.Samples.NavigationController/Properties/Settings.settings new file mode 100644 index 0000000..8f2fd95 --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.NavigationController/Stylet.Samples.NavigationController.csproj b/Samples/Stylet.Samples.NavigationController/Stylet.Samples.NavigationController.csproj new file mode 100644 index 0000000..844f7de --- /dev/null +++ b/Samples/Stylet.Samples.NavigationController/Stylet.Samples.NavigationController.csproj @@ -0,0 +1,119 @@ + + + + + Debug + AnyCPU + {9DEE75AC-6BC1-4375-B6A0-44EAF39FF559} + WinExe + Stylet.Samples.NavigationController + Stylet.Samples.NavigationController + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + + + App.xaml + Code + + + + + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.sln b/Samples/Stylet.Samples.sln index 569a4d4..d991146 100644 --- a/Samples/Stylet.Samples.sln +++ b/Samples/Stylet.Samples.sln @@ -1,70 +1,79 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.23107.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet", "..\Stylet\Stylet.csproj", "{2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.Hello", "Stylet.Samples.Hello\Stylet.Samples.Hello.csproj", "{6C7FBB21-52AC-4333-A42A-9F5E9D048621}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.TabNavigation", "Stylet.Samples.TabNavigation\Stylet.Samples.TabNavigation.csproj", "{9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.MasterDetail", "Stylet.Samples.MasterDetail\Stylet.Samples.MasterDetail.csproj", "{A281DFF2-125E-4412-8927-0F09EEFC5AD1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.HelloDialog", "Stylet.Samples.HelloDialog\Stylet.Samples.HelloDialog.csproj", "{F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.OverridingViewManager", "Stylet.Samples.OverridingViewManager\Stylet.Samples.OverridingViewManager.csproj", "{2F7D7EF3-730A-45E3-93CA-7C5031250246}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.RedditBrowser", "Stylet.Samples.RedditBrowser\Stylet.Samples.RedditBrowser.csproj", "{72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.ModelValidation", "Stylet.Samples.ModelValidation\Stylet.Samples.ModelValidation.csproj", "{EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.DesignMode", "Stylet.Samples.DesignMode\Stylet.Samples.DesignMode.csproj", "{D5225DA1-58ED-42AA-9589-A4F86E7667F7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Release|Any CPU.Build.0 = Release|Any CPU - {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Release|Any CPU.Build.0 = Release|Any CPU - {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Release|Any CPU.Build.0 = Release|Any CPU - {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Release|Any CPU.Build.0 = Release|Any CPU - {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Release|Any CPU.Build.0 = Release|Any CPU - {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Release|Any CPU.Build.0 = Release|Any CPU - {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Release|Any CPU.Build.0 = Release|Any CPU - {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30709.64 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stylet", "..\Stylet\Stylet.csproj", "{2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.Hello", "Stylet.Samples.Hello\Stylet.Samples.Hello.csproj", "{6C7FBB21-52AC-4333-A42A-9F5E9D048621}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.TabNavigation", "Stylet.Samples.TabNavigation\Stylet.Samples.TabNavigation.csproj", "{9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.MasterDetail", "Stylet.Samples.MasterDetail\Stylet.Samples.MasterDetail.csproj", "{A281DFF2-125E-4412-8927-0F09EEFC5AD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.HelloDialog", "Stylet.Samples.HelloDialog\Stylet.Samples.HelloDialog.csproj", "{F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.OverridingViewManager", "Stylet.Samples.OverridingViewManager\Stylet.Samples.OverridingViewManager.csproj", "{2F7D7EF3-730A-45E3-93CA-7C5031250246}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.RedditBrowser", "Stylet.Samples.RedditBrowser\Stylet.Samples.RedditBrowser.csproj", "{72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.ModelValidation", "Stylet.Samples.ModelValidation\Stylet.Samples.ModelValidation.csproj", "{EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.DesignMode", "Stylet.Samples.DesignMode\Stylet.Samples.DesignMode.csproj", "{D5225DA1-58ED-42AA-9589-A4F86E7667F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylet.Samples.NavigationController", "Stylet.Samples.NavigationController\Stylet.Samples.NavigationController.csproj", "{9DEE75AC-6BC1-4375-B6A0-44EAF39FF559}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5}.Release|Any CPU.Build.0 = Release|Any CPU + {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C7FBB21-52AC-4333-A42A-9F5E9D048621}.Release|Any CPU.Build.0 = Release|Any CPU + {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE}.Release|Any CPU.Build.0 = Release|Any CPU + {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A281DFF2-125E-4412-8927-0F09EEFC5AD1}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44}.Release|Any CPU.Build.0 = Release|Any CPU + {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F7D7EF3-730A-45E3-93CA-7C5031250246}.Release|Any CPU.Build.0 = Release|Any CPU + {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA}.Release|Any CPU.Build.0 = Release|Any CPU + {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA5A6CA5-7E8E-4401-A3D2-0035DDE10413}.Release|Any CPU.Build.0 = Release|Any CPU + {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5225DA1-58ED-42AA-9589-A4F86E7667F7}.Release|Any CPU.Build.0 = Release|Any CPU + {9DEE75AC-6BC1-4375-B6A0-44EAF39FF559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DEE75AC-6BC1-4375-B6A0-44EAF39FF559}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DEE75AC-6BC1-4375-B6A0-44EAF39FF559}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DEE75AC-6BC1-4375-B6A0-44EAF39FF559}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A72165C7-7E4E-4336-88FF-BDCC3E300D54} + EndGlobalSection +EndGlobal diff --git a/Stylet/Bootstrapper.cs b/Stylet/Bootstrapper.cs index 9024a88..2d0f381 100644 --- a/Stylet/Bootstrapper.cs +++ b/Stylet/Bootstrapper.cs @@ -8,14 +8,12 @@ namespace Stylet /// /// Bootstrapper to be extended by any application which wants to use StyletIoC (the default) /// + /// + /// If you don't have a root ViewModel, you might prefer to override + /// /// Type of the root ViewModel. This will be instantiated and displayed - public abstract class Bootstrapper : BootstrapperBase where TRootViewModel : class + public abstract class Bootstrapper : StyletIoCBootstrapperBase where TRootViewModel : class { - /// - /// Gets or sets the Bootstrapper's IoC container. This is created after ConfigureIoC has been run. - /// - protected IContainer Container { get; set; } - private TRootViewModel _rootViewModel; /// @@ -25,65 +23,7 @@ namespace Stylet { get { return this._rootViewModel ?? (this._rootViewModel = this.Container.Get()); } } - - /// - /// Overridden from BootstrapperBase, this sets up the IoC container - /// - protected override sealed void ConfigureBootstrapper() - { - var builder = new StyletIoCBuilder(); - builder.Assemblies = new List(new List() { this.GetType().Assembly }); - - // Call DefaultConfigureIoC *after* ConfigureIoIC, so that they can customize builder.Assemblies - this.ConfigureIoC(builder); - this.DefaultConfigureIoC(builder); - - this.Container = builder.BuildContainer(); - } - - /// - /// Carries out default configuration of StyletIoC. Override if you don't want to do this - /// - /// StyletIoC builder to use to configure the container - protected virtual void DefaultConfigureIoC(StyletIoCBuilder builder) - { - // Mark these as weak-bindings, so the user can replace them if they want - var viewManagerConfig = new ViewManagerConfig() - { - ViewFactory = this.GetInstance, - ViewAssemblies = new List() { this.GetType().Assembly } - }; - builder.Bind().ToInstance(viewManagerConfig).AsWeakBinding(); - - // Bind it to both IViewManager and to itself, so that people can get it with Container.Get() - builder.Bind().And().To().InSingletonScope().AsWeakBinding(); - - builder.Bind().ToInstance(this).DisposeWithContainer(false).AsWeakBinding(); - builder.Bind().To().InSingletonScope().AsWeakBinding(); - builder.Bind().To().InSingletonScope().AsWeakBinding(); - builder.Bind().To().AsWeakBinding(); - // Stylet's assembly isn't added to the container, so add this explicitly - builder.Bind().ToSelf(); - - builder.Autobind(); - } - - /// - /// Override to add your own types to the IoC container. - /// - /// StyletIoC builder to use to configure the container - protected virtual void ConfigureIoC(IStyletIoCBuilder builder) { } - - /// - /// Given a type, use the IoC container to fetch an instance of it - /// - /// Type to fetch - /// Fetched instance - public override object GetInstance(Type type) - { - return this.Container.Get(type); - } - + /// /// Called when the application is launched. Displays the root view. /// @@ -97,12 +37,11 @@ namespace Stylet /// public override void Dispose() { - // Dispose the container last - base.Dispose(); // Don't create the root ViewModel if it doesn't already exist... ScreenExtensions.TryDispose(this._rootViewModel); - if (this.Container != null) - this.Container.Dispose(); + + // Dispose the container last + base.Dispose(); } } } diff --git a/Stylet/BootstrapperBase.cs b/Stylet/BootstrapperBase.cs index 5f3ff29..63d2efa 100644 --- a/Stylet/BootstrapperBase.cs +++ b/Stylet/BootstrapperBase.cs @@ -34,6 +34,13 @@ namespace Stylet /// /// Called by the ApplicationLoader when this bootstrapper is loaded /// + /// + /// If you're constructing the bootstrapper yourself, call this manully and pass in the Application + /// (probably ). Stylet will start when + /// is fired. If no Application is available, do not call this but instead call . + /// (In this case, note that the methods will all dispatch synchronously, unless you + /// set yourself). + /// /// Application within which Stylet is running public void Setup(Application application) { @@ -43,7 +50,7 @@ namespace Stylet this.Application = application; // Use the current application's dispatcher for Execute - Execute.Dispatcher = new DispatcherWrapper(this.Application.Dispatcher); + Execute.Dispatcher = new ApplicationDispatcher(this.Application.Dispatcher); this.Application.Startup += (o, e) => this.Start(e.Args); // Make life nice for the app - they can handle these by overriding Bootstrapper methods, rather than adding event handlers @@ -64,6 +71,10 @@ namespace Stylet /// /// Called on Application.Startup, this does everything necessary to start the application /// + /// + /// If you're constructing the bootstrapper yourself, and aren't able to call , + /// (e.g. because an Application isn't available), you must call this yourself. + /// /// Command-line arguments used to start this executable public virtual void Start(string[] args) { @@ -73,9 +84,8 @@ namespace Stylet this.ConfigureBootstrapper(); - // Cater for the unit tests, which can't sensibly stub Application - if (this.Application != null) - this.Application.Resources.Add(View.ViewManagerResourceKey, this.GetInstance(typeof(IViewManager))); + // We allow starting without an application + this.Application?.Resources.Add(View.ViewManagerResourceKey, this.GetInstance(typeof(IViewManager))); this.Configure(); this.Launch(); @@ -107,7 +117,7 @@ namespace Stylet /// The currently-displayed window, or null public virtual Window GetActiveWindow() { - return this.Application.Windows.OfType().FirstOrDefault(x => x.IsActive) ?? this.Application.MainWindow; + return this.Application?.Windows.OfType().FirstOrDefault(x => x.IsActive) ?? this.Application?.MainWindow; } /// diff --git a/Stylet/Execute.cs b/Stylet/Execute.cs index 3000de3..4054c27 100644 --- a/Stylet/Execute.cs +++ b/Stylet/Execute.cs @@ -16,11 +16,13 @@ namespace Stylet /// Gets or sets Execute's dispatcher /// /// - /// Should be set to the UI thread's Dispatcher. This is normally done by the Bootstrapper. + /// Should be set a wrapping the current application's dispatcher, which is + /// normally done by the Bootstrapper. Can also be set to , or a + /// custom implementation. /// public static IDispatcher Dispatcher { - get { return _dispatcher ?? (_dispatcher = new SynchronousDispatcher()); } + get { return _dispatcher ?? SynchronousDispatcher.Instance; } set { diff --git a/Stylet/IDispatcher.cs b/Stylet/IDispatcher.cs index bab53a5..da64d02 100644 --- a/Stylet/IDispatcher.cs +++ b/Stylet/IDispatcher.cs @@ -1,10 +1,12 @@ using System; +using System.Windows; using System.Windows.Threading; namespace Stylet { /// - /// Generalised dispatcher, which can post and send + /// Generalised dispatcher, which can post and send. + /// Used by . /// public interface IDispatcher { @@ -26,43 +28,75 @@ namespace Stylet bool IsCurrent { get; } } - internal class DispatcherWrapper : IDispatcher + /// + /// implementation which can dispatch using + /// + public class ApplicationDispatcher : IDispatcher { private readonly Dispatcher dispatcher; - public DispatcherWrapper(Dispatcher dispatcher) + /// + /// Initialises a new instance of the class with the given + /// + /// to use, normally Application.Current.Dispatcher + public ApplicationDispatcher(Dispatcher dispatcher) { - this.dispatcher = dispatcher; + this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); } + /// + /// Initialises a new instance of the class with the given + /// + /// to use, normally Application + public ApplicationDispatcher(Application application) + : this(application?.Dispatcher ?? throw new ArgumentNullException(nameof(application))) + { + } + + /// public void Post(Action action) { this.dispatcher.BeginInvoke(action); } + /// public void Send(Action action) { this.dispatcher.Invoke(action); } + /// public bool IsCurrent { get { return this.dispatcher.CheckAccess(); } } } - internal class SynchronousDispatcher : IDispatcher + /// + /// implementation whcih dispatches synchronously. + /// Usually used for unit testing. + /// + public class SynchronousDispatcher : IDispatcher { + /// + /// Gets the singleton instance of + /// + public static SynchronousDispatcher Instance { get; } = new SynchronousDispatcher(); + private SynchronousDispatcher() { } + + /// public void Post(Action action) { action(); } + /// public void Send(Action action) { action(); } + /// public bool IsCurrent { get { return true; } diff --git a/Stylet/Stylet.csproj b/Stylet/Stylet.csproj index 6cb4196..14b2834 100644 --- a/Stylet/Stylet.csproj +++ b/Stylet/Stylet.csproj @@ -1,4 +1,4 @@ - + Library @@ -23,16 +23,12 @@ + snupkg true portable - - - full - - diff --git a/Stylet/StyletIoCBootstrapperBase.cs b/Stylet/StyletIoCBootstrapperBase.cs new file mode 100644 index 0000000..14cb186 --- /dev/null +++ b/Stylet/StyletIoCBootstrapperBase.cs @@ -0,0 +1,93 @@ +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Stylet +{ + /// + /// Bootstrapper to be extended by any application which wants to use StyletIoC, but doesn't have a root ViewModel + /// + /// + /// You would normally use , which lets you specify the root ViewModel + /// to display. If you don't want to show a window on startup, override + /// but don't call . + /// + public abstract class StyletIoCBootstrapperBase : BootstrapperBase + { + /// + /// Gets or sets the Bootstrapper's IoC container. This is created after ConfigureIoC has been run. + /// + protected IContainer Container { get; set; } + + /// + /// Overridden from BootstrapperBase, this sets up the IoC container + /// + protected override sealed void ConfigureBootstrapper() + { + var builder = new StyletIoCBuilder(); + builder.Assemblies = new List(new List() { this.GetType().Assembly }); + + // Call DefaultConfigureIoC *after* ConfigureIoIC, so that they can customize builder.Assemblies + this.ConfigureIoC(builder); + this.DefaultConfigureIoC(builder); + + this.Container = builder.BuildContainer(); + } + + /// + /// Carries out default configuration of StyletIoC. Override if you don't want to do this + /// + /// StyletIoC builder to use to configure the container + protected virtual void DefaultConfigureIoC(StyletIoCBuilder builder) + { + // Mark these as weak-bindings, so the user can replace them if they want + var viewManagerConfig = new ViewManagerConfig() + { + ViewFactory = this.GetInstance, + ViewAssemblies = new List() { this.GetType().Assembly } + }; + builder.Bind().ToInstance(viewManagerConfig).AsWeakBinding(); + + // Bind it to both IViewManager and to itself, so that people can get it with Container.Get() + builder.Bind().And().To().InSingletonScope().AsWeakBinding(); + + builder.Bind().ToInstance(this).DisposeWithContainer(false).AsWeakBinding(); + builder.Bind().To().InSingletonScope().AsWeakBinding(); + builder.Bind().To().InSingletonScope().AsWeakBinding(); + builder.Bind().To().AsWeakBinding(); + // Stylet's assembly isn't added to the container, so add this explicitly + builder.Bind().ToSelf(); + + builder.Autobind(); + } + + /// + /// Override to add your own types to the IoC container. + /// + /// StyletIoC builder to use to configure the container + protected virtual void ConfigureIoC(IStyletIoCBuilder builder) { } + + /// + /// Given a type, use the IoC container to fetch an instance of it + /// + /// Type to fetch + /// Fetched instance + public override object GetInstance(Type type) + { + return this.Container.Get(type); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public override void Dispose() + { + base.Dispose(); + + // Dispose the container last + if (this.Container != null) + this.Container.Dispose(); + } + } +} diff --git a/Stylet/Xaml/ActionBase.cs b/Stylet/Xaml/ActionBase.cs index a514c45..1017cfb 100644 --- a/Stylet/Xaml/ActionBase.cs +++ b/Stylet/Xaml/ActionBase.cs @@ -3,7 +3,9 @@ using System; using System.Diagnostics; using System.Globalization; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using System.Windows; using System.Windows.Data; @@ -42,11 +44,12 @@ namespace Stylet.Xaml protected readonly ActionUnavailableBehaviour ActionNonExistentBehaviour; /// - /// Gets the object on which methods will be invokced + /// Gets the object on which methods will be invoked /// public object Target { get { return this.GetValue(targetProperty); } + private set { this.SetValue(targetProperty, value); } } private static readonly DependencyProperty targetProperty = @@ -56,7 +59,7 @@ namespace Stylet.Xaml })); /// - /// Initialises a new instance of the class + /// Initialises a new instance of the class to use to get the target /// /// View to grab the View.ActionTarget from /// Backup subject to use if no ActionTarget could be retrieved from the subject @@ -65,12 +68,9 @@ namespace Stylet.Xaml /// Behaviour for if the action doesn't exist on the View.ActionTarget /// Logger to use public ActionBase(DependencyObject subject, DependencyObject backupSubject, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger) + : this(methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) { this.Subject = subject; - this.MethodName = methodName; - this.TargetNullBehaviour = targetNullBehaviour; - this.ActionNonExistentBehaviour = actionNonExistentBehaviour; - this.logger = logger; // If a 'backupSubject' was given, bind both that and 'subject' to this.Target (with a converter which picks the first // one that isn't View.InitialActionTarget). If it wasn't given, just bind 'subject'. @@ -101,6 +101,31 @@ namespace Stylet.Xaml } } + /// + /// Initialises a new instance of the class to use an explicit target + /// + /// Target to find the method on + /// Method name. the MyMethod in Buttom Command="{s:Action MyMethod}". + /// Behaviour for it the relevant View.ActionTarget is null + /// Behaviour for if the action doesn't exist on the View.ActionTarget + /// Logger to use + public ActionBase(object target, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger) + : this(methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + this.Target = target; + } + + private ActionBase(string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour, ILogger logger) + { + this.MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + this.TargetNullBehaviour = targetNullBehaviour; + this.ActionNonExistentBehaviour = actionNonExistentBehaviour; + this.logger = logger; + } + private void UpdateActionTarget(object oldTarget, object newTarget) { MethodInfo targetMethodInfo = null; @@ -128,13 +153,22 @@ namespace Stylet.Xaml } else { - var newTargetType = newTarget.GetType(); + BindingFlags bindingFlags; + if (newTarget is Type newTargetType) + { + bindingFlags = BindingFlags.Public | BindingFlags.Static; + } + else + { + newTargetType = newTarget.GetType(); + bindingFlags = BindingFlags.Public | BindingFlags.Instance; + } try { - targetMethodInfo = newTargetType.GetMethod(this.MethodName); + targetMethodInfo = newTargetType.GetMethod(this.MethodName, bindingFlags); if (targetMethodInfo == null) - this.logger.Warn("Unable to find method {0} on {1}", this.MethodName, newTargetType.Name); + this.logger.Warn("Unable to find{0} method {1} on {2}", newTarget is Type ? " static" : "", this.MethodName, newTargetType.Name); else this.AssertTargetMethodInfo(targetMethodInfo, newTargetType); } @@ -156,19 +190,19 @@ namespace Stylet.Xaml /// /// MethodInfo of method on new target /// Type of new target - protected internal abstract void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType); + private protected abstract void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType); /// /// Invoked when a new target is set, after all other action has been taken /// /// Previous target /// New target - protected internal virtual void OnTargetChanged(object oldTarget, object newTarget) { } + private protected virtual void OnTargetChanged(object oldTarget, object newTarget) { } /// /// Assert that the target is not View.InitialActionTarget /// - protected internal void AssertTargetSet() + private protected void AssertTargetSet() { // If we've made it this far and the target is still the default, then something's wrong // Make sure they know @@ -183,7 +217,7 @@ namespace Stylet.Xaml if (this.TargetMethodInfo == null && this.ActionNonExistentBehaviour == ActionUnavailableBehaviour.Throw) { - var ex = new ActionNotFoundException(String.Format("Unable to find method {0} on target {1}", this.MethodName, this.Target.GetType().Name)); + var ex = new ActionNotFoundException(String.Format("Unable to find method {0} on {1}", this.MethodName, this.TargetName())); this.logger.Error(ex); throw ex; } @@ -193,22 +227,37 @@ namespace Stylet.Xaml /// Invoke the target method with the given parameters /// /// Parameters to pass to the target method - protected internal void InvokeTargetMethod(object[] parameters) + private protected void InvokeTargetMethod(object[] parameters) { - this.logger.Info("Invoking method {0} on target {1} with parameters ({2})", this.MethodName, this.Target, parameters == null ? "none" : String.Join(", ", parameters)); + this.logger.Info("Invoking method {0} on {1} with parameters ({2})", this.MethodName, this.TargetName(), parameters == null ? "none" : String.Join(", ", parameters)); try { - this.TargetMethodInfo.Invoke(this.Target, parameters); + var target = this.TargetMethodInfo.IsStatic ? null : this.Target; + var result = this.TargetMethodInfo.Invoke(target, parameters); + // Be nice and make sure that any exceptions get rethrown + if (result is Task task) + { + AwaitTask(task); + } } catch (TargetInvocationException e) { // Be nice and unwrap this for them // They want a stack track for their VM method, not us - this.logger.Error(e.InnerException, String.Format("Failed to invoke method {0} on target {1} with parameters ({2})", this.MethodName, this.Target, parameters == null ? "none" : String.Join(", ", parameters))); + this.logger.Error(e.InnerException, String.Format("Failed to invoke method {0} on {1} with parameters ({2})", this.MethodName, this.TargetName(), parameters == null ? "none" : String.Join(", ", parameters))); // http://stackoverflow.com/a/17091351/1086121 ExceptionDispatchInfo.Capture(e.InnerException).Throw(); } + + async void AwaitTask(Task t) => await t; + } + + private string TargetName() + { + return this.Target is Type t + ? $"static target {t.Name}" + : $"target {this.Target.GetType().Name}"; } private class MultiBindingToActionTargetConverter : IMultiValueConverter diff --git a/Stylet/Xaml/ActionExtension.cs b/Stylet/Xaml/ActionExtension.cs index 16ac98e..def8cba 100644 --- a/Stylet/Xaml/ActionExtension.cs +++ b/Stylet/Xaml/ActionExtension.cs @@ -44,6 +44,11 @@ namespace Stylet.Xaml [ConstructorArgument("method")] public string Method { get; set; } + /// + /// Gets or sets a target to override that set with View.ActionTarget + /// + public object Target { get; set; } + /// /// Gets or sets the behaviour if the View.ActionTarget is nulil /// @@ -101,15 +106,13 @@ namespace Stylet.Xaml throw new InvalidOperationException("Method has not been set"); var valueService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); - var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider)); - var rootObject = rootObjectProvider?.RootObject as DependencyObject; switch (valueService.TargetObject) { case DependencyObject targetObject: - return this.HandleDependencyObject(valueService, targetObject, rootObject); + return this.HandleDependencyObject(serviceProvider, valueService, targetObject); case CommandBinding commandBinding: - return this.HandleCommandBinding(rootObject, ((EventInfo)valueService.TargetProperty).EventHandlerType); + return this.CreateEventAction(serviceProvider, null, ((EventInfo)valueService.TargetProperty).EventHandlerType, isCommandBinding: true); default: // Seems this is the case when we're in a template. We'll get called again properly in a second. // http://social.msdn.microsoft.com/Forums/vstudio/en-US/a9ead3d5-a4e4-4f9c-b507-b7a7d530c6a9/gaining-access-to-target-object-instead-of-shareddp-in-custom-markupextensions-providevalue-method?forum=wpf @@ -117,39 +120,66 @@ namespace Stylet.Xaml } } - private object HandleDependencyObject(IProvideValueTarget valueService, DependencyObject targetObject, DependencyObject rootObject) + private object HandleDependencyObject(IServiceProvider serviceProvider, IProvideValueTarget valueService, DependencyObject targetObject) { switch (valueService.TargetProperty) { case DependencyProperty dependencyProperty when dependencyProperty.PropertyType == typeof(ICommand): // If they're in design mode and haven't set View.ActionTarget, default to looking sensible - return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour); + return this.CreateCommandAction(serviceProvider, targetObject); case EventInfo eventInfo: - { - var ec = new EventAction(targetObject, rootObject, eventInfo.EventHandlerType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); - return ec.GetDelegate(); - } + return this.CreateEventAction(serviceProvider, targetObject, eventInfo.EventHandlerType); case MethodInfo methodInfo: // For attached events - { - var parameters = methodInfo.GetParameters(); - if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType)) { - var ec = new EventAction(targetObject, rootObject, parameters[1].ParameterType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); - return ec.GetDelegate(); + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 2 && typeof(Delegate).IsAssignableFrom(parameters[1].ParameterType)) + { + return this.CreateEventAction(serviceProvider, targetObject, parameters[1].ParameterType); + } + throw new ArgumentException("Action used with an attached event (or something similar) which didn't follow the normal pattern"); } - throw new ArgumentException("Action used with an attached event (or something similar) which didn't follow the normal pattern"); - } default: throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler"); } } - private object HandleCommandBinding(DependencyObject rootObject, Type propertyType) + private ICommand CreateCommandAction(IServiceProvider serviceProvider, DependencyObject targetObject) { - if (rootObject == null) - throw new InvalidOperationException("Action may only be used with CommandBinding from a XAML view (unable to retrieve IRootObjectProvider.RootObject)"); + if (this.Target == null) + { + var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider)); + var rootObject = rootObjectProvider?.RootObject as DependencyObject; + return new CommandAction(targetObject, rootObject, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour); + } + else + { + return new CommandAction(this.Target, this.Method, this.CommandNullTargetBehaviour, this.CommandActionNotFoundBehaviour); + } + } + + private Delegate CreateEventAction(IServiceProvider serviceProvider, DependencyObject targetObject, Type eventType, bool isCommandBinding = false) + { + EventAction ec; + if (this.Target == null) + { + var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider)); + var rootObject = rootObjectProvider?.RootObject as DependencyObject; + if (isCommandBinding) + { + if (rootObject == null) + throw new InvalidOperationException("Action may only be used with CommandBinding from a XAML view (unable to retrieve IRootObjectProvider.RootObject)"); + ec = new EventAction(rootObject, null, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + } + else + { + ec = new EventAction(targetObject, rootObject, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + } + } + else + { + ec = new EventAction(this.Target, eventType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); + } - var ec = new EventAction(rootObject, null, propertyType, this.Method, this.EventNullTargetBehaviour, this.EventActionNotFoundBehaviour); return ec.GetDelegate(); } } diff --git a/Stylet/Xaml/CommandAction.cs b/Stylet/Xaml/CommandAction.cs index a2db63a..b03cbac 100644 --- a/Stylet/Xaml/CommandAction.cs +++ b/Stylet/Xaml/CommandAction.cs @@ -26,7 +26,7 @@ namespace Stylet.Xaml private Func guardPropertyGetter; /// - /// Initialises a new instance of the class + /// Initialises a new instance of the class to use to get the target /// /// View to grab the View.ActionTarget from /// Backup subject to use if no ActionTarget could be retrieved from the subject @@ -37,6 +37,17 @@ namespace Stylet.Xaml : base(subject, backupSubject, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) { } + /// + /// Initialises a new instance of the class to use an explicit target + /// + /// Target to find the method on + /// Method name. the MyMethod in Buttom Command="{s:Action MyMethod}". + /// Behaviour for it the relevant View.ActionTarget is null + /// Behaviour for if the action doesn't exist on the View.ActionTarget + public CommandAction(object target, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour) + : base(target, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) + { } + private string GuardName { get { return "Can" + this.MethodName; } @@ -47,7 +58,7 @@ namespace Stylet.Xaml /// /// MethodInfo of method on new target /// Type of new target - protected internal override void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType) + private protected override void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType) { var methodParameters = targetMethodInfo.GetParameters(); if (methodParameters.Length > 1) @@ -63,34 +74,33 @@ namespace Stylet.Xaml /// /// Previous target /// New target - protected internal override void OnTargetChanged(object oldTarget, object newTarget) + private protected override void OnTargetChanged(object oldTarget, object newTarget) { - var oldInpc = oldTarget as INotifyPropertyChanged; - if (oldInpc != null) + if (oldTarget is INotifyPropertyChanged oldInpc) PropertyChangedEventManager.RemoveHandler(oldInpc, this.PropertyChangedHandler, this.GuardName); this.guardPropertyGetter = null; - - var inpc = newTarget as INotifyPropertyChanged; - if (inpc != null) + var guardPropertyInfo = newTarget?.GetType().GetProperty(this.GuardName); + if (guardPropertyInfo != null) { - var guardPropertyInfo = newTarget.GetType().GetProperty(this.GuardName); - if (guardPropertyInfo != null) + if (guardPropertyInfo.PropertyType == typeof(bool)) { - if (guardPropertyInfo.PropertyType == typeof(bool)) - { - var targetExpression = Expressions.Expression.Constant(newTarget); - var propertyAccess = Expressions.Expression.Property(targetExpression, guardPropertyInfo); - this.guardPropertyGetter = Expressions.Expression.Lambda>(propertyAccess).Compile(); - } - else - { - logger.Warn("Found guard property {0} for action {1} on target {2}, but its return type wasn't bool. Therefore, ignoring", this.GuardName, this.MethodName, newTarget); - } + var targetExpression = Expressions.Expression.Constant(newTarget); + var propertyAccess = Expressions.Expression.Property(targetExpression, guardPropertyInfo); + this.guardPropertyGetter = Expressions.Expression.Lambda>(propertyAccess).Compile(); } + else + { + logger.Warn("Found guard property {0} for action {1} on target {2}, but its return type wasn't bool. Therefore, ignoring", this.GuardName, this.MethodName, newTarget); + } + } - if (this.guardPropertyGetter != null) + if (this.guardPropertyGetter != null) + { + if (newTarget is INotifyPropertyChanged inpc) PropertyChangedEventManager.AddHandler(inpc, this.PropertyChangedHandler, this.GuardName); + else + logger.Warn("Found guard property {0} for action {1} on target {2}, but the target doesn't implement INotifyPropertyChanged, so changes won't be observed", this.GuardName, this.MethodName, newTarget); } this.UpdateCanExecute(); diff --git a/Stylet/Xaml/EventAction.cs b/Stylet/Xaml/EventAction.cs index 367c2f3..eba0acf 100644 --- a/Stylet/Xaml/EventAction.cs +++ b/Stylet/Xaml/EventAction.cs @@ -23,7 +23,7 @@ namespace Stylet.Xaml private readonly Type eventHandlerType; /// - /// Initialises a new instance of the class + /// Initialises a new instance of the classto use to get the target /// /// View whose View.ActionTarget we watch /// Backup subject to use if no ActionTarget could be retrieved from the subject @@ -33,13 +33,32 @@ namespace Stylet.Xaml /// Behaviour for if the action doesn't exist on the View.ActionTarget public EventAction(DependencyObject subject, DependencyObject backupSubject, Type eventHandlerType, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour) : base(subject, backupSubject, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) + { + AssertBehaviours(targetNullBehaviour, actionNonExistentBehaviour); + this.eventHandlerType = eventHandlerType; + } + + /// + /// Initialises a new instance of the class to use an explicit target + /// + /// Target to find the method on + /// Type of event handler we're returning a delegate for + /// The MyMethod in {s:Action MyMethod}, this is what we call when the event's fired + /// Behaviour for it the relevant View.ActionTarget is null + /// Behaviour for if the action doesn't exist on the View.ActionTarget + public EventAction(object target, Type eventHandlerType, string methodName, ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour) + : base(target, methodName, targetNullBehaviour, actionNonExistentBehaviour, logger) + { + AssertBehaviours(targetNullBehaviour, actionNonExistentBehaviour); + this.eventHandlerType = eventHandlerType; + } + + private static void AssertBehaviours(ActionUnavailableBehaviour targetNullBehaviour, ActionUnavailableBehaviour actionNonExistentBehaviour) { if (targetNullBehaviour == ActionUnavailableBehaviour.Disable) throw new ArgumentException("Setting NullTarget = Disable is unsupported when used on an Event"); if (actionNonExistentBehaviour == ActionUnavailableBehaviour.Disable) throw new ArgumentException("Setting ActionNotFound = Disable is unsupported when used on an Event"); - - this.eventHandlerType = eventHandlerType; } /// @@ -47,14 +66,14 @@ namespace Stylet.Xaml /// /// MethodInfo of method on new target /// Type of new target - protected internal override void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType) + private protected override void AssertTargetMethodInfo(MethodInfo targetMethodInfo, Type newTargetType) { var methodParameters = targetMethodInfo.GetParameters(); if (!(methodParameters.Length == 0 || (methodParameters.Length == 1 && (typeof(EventArgs).IsAssignableFrom(methodParameters[0].ParameterType) || methodParameters[0].ParameterType == typeof(DependencyPropertyChangedEventArgs))) || (methodParameters.Length == 2 && (typeof(EventArgs).IsAssignableFrom(methodParameters[1].ParameterType) || methodParameters[1].ParameterType == typeof(DependencyPropertyChangedEventArgs))))) { - var e = new ActionSignatureInvalidException(String.Format("Method {0} on {1} must have the signatures void Method(), void Method(EventArgsOrSubClass e), or void Method(object sender, EventArgsOrSubClass e)", this.MethodName, newTargetType.Name)); + var e = new ActionSignatureInvalidException(String.Format("Method {0} on {1} must have the signatures Method(), Method(EventArgsOrSubClass e), or Method(object sender, EventArgsOrSubClass e)", this.MethodName, newTargetType.Name)); logger.Error(e); throw e; } diff --git a/StyletUnitTests/ActionExtensionTests.cs b/StyletUnitTests/ActionExtensionTests.cs index 1b3f174..82d3260 100644 --- a/StyletUnitTests/ActionExtensionTests.cs +++ b/StyletUnitTests/ActionExtensionTests.cs @@ -126,6 +126,19 @@ namespace StyletUnitTests Assert.Throws(() => this.actionExtension.ProvideValue(this.serviceProvider.Object)); } + + [Test] + public void OverridesTargetIfSetCommand() + { + var target = new object(); + this.actionExtension.Target = target; + + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(Button.CommandProperty); + var cmd = (CommandAction)this.actionExtension.ProvideValue(this.serviceProvider.Object); + Assert.AreSame(target, cmd.Target); + } + + // Can't really test Target on EventAction. Oh well. } } diff --git a/StyletUnitTests/CommandActionTests.cs b/StyletUnitTests/CommandActionTests.cs index 876ec82..28311e7 100644 --- a/StyletUnitTests/CommandActionTests.cs +++ b/StyletUnitTests/CommandActionTests.cs @@ -2,6 +2,7 @@ using Stylet; using Stylet.Xaml; using System; +using System.Runtime.CompilerServices; using System.Windows; namespace StyletUnitTests @@ -23,8 +24,11 @@ namespace StyletUnitTests get { return this._canDoSomethingWithGuard; } set { SetAndNotify(ref this._canDoSomethingWithGuard, value); } } + + public bool DoSomethingWithGuardCalled; public void DoSomethingWithGuard() { + this.DoSomethingWithGuardCalled = true; } public object DoSomethingArgument; @@ -62,6 +66,18 @@ namespace StyletUnitTests { } + private class TargetWithoutInpc + { + public bool CanDoSomething => false; + public void DoSomething() { } + } + + public class StaticTarget + { + public static bool DidSomething; + public static void DoSomething() => DidSomething = true; + } + private DependencyObject subject; private Target target; @@ -71,6 +87,7 @@ namespace StyletUnitTests this.target = new Target(); this.subject = new DependencyObject(); View.SetActionTarget(this.subject, this.target); + StaticTarget.DidSomething = false; } [Test] @@ -171,6 +188,18 @@ namespace StyletUnitTests Assert.True(eventRaised); } + [Test] + public void FetchesGuardPropertyWhenTargetDoesNotImplementInpc() + { + var target = new TargetWithoutInpc(); + var cmd = new CommandAction(this.subject, null, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + bool eventRaised = false; + cmd.CanExecuteChanged += (o, e) => eventRaised = true; + View.SetActionTarget(this.subject, target); + Assert.True(eventRaised); + Assert.False(cmd.CanExecute(null)); + } + [Test] public void RaisesEventWhenTargetChanges() { @@ -297,5 +326,29 @@ namespace StyletUnitTests cmd.Execute(null); Assert.IsTrue(this.target.DoSomethingCalled); } + + [Test] + public void SupportsStaticTargets() + { + var cmd = new CommandAction(this.subject, null, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + View.SetActionTarget(this.subject, typeof(StaticTarget)); + + Assert.True(cmd.CanExecute(null)); + cmd.Execute(null); + Assert.True(StaticTarget.DidSomething); + } + + [Test] + public void UsesExplicitTarget() + { + var cmd = new CommandAction(this.target, "DoSomethingWithGuard", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + + Assert.False(cmd.CanExecute(null)); + this.target.CanDoSomethingWithGuard = true; + Assert.True(cmd.CanExecute(null)); + + cmd.Execute(null); + Assert.True(this.target.DoSomethingWithGuardCalled); + } } } diff --git a/StyletUnitTests/EventActionTests.cs b/StyletUnitTests/EventActionTests.cs index ab18b59..970ce11 100644 --- a/StyletUnitTests/EventActionTests.cs +++ b/StyletUnitTests/EventActionTests.cs @@ -74,6 +74,12 @@ namespace StyletUnitTests { } + public class StaticTarget + { + public static bool DidSomething; + public static void DoSomething() => DidSomething = true; + } + private DependencyObject subject; private Target target; private EventInfo eventInfo; @@ -87,6 +93,7 @@ namespace StyletUnitTests this.eventInfo = typeof(Subject).GetEvent("SimpleEventHandler"); this.dependencyChangedEventInfo = typeof(Subject).GetEvent("DependencyChangedEventHandler"); View.SetActionTarget(this.subject, this.target); + StaticTarget.DidSomething = false; } [Test] @@ -269,13 +276,31 @@ namespace StyletUnitTests { var view = new DependencyObject(); var backupView = new DependencyObject(); - var cmd = new CommandAction(view, backupView, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + var cmd = new EventAction(view, backupView, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); View.SetActionTarget(backupView, this.target); view.SetValue(FrameworkElement.DataContextProperty, this.target); - cmd.Execute(null); + cmd.GetDelegate().DynamicInvoke(null, null); Assert.IsTrue(this.target.DoSomethingCalled); } + + [Test] + public void SupportsStaticTargets() + { + var cmd = new EventAction(this.subject, null, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + View.SetActionTarget(this.subject, typeof(StaticTarget)); + + cmd.GetDelegate().DynamicInvoke(null, null); + Assert.True(StaticTarget.DidSomething); + } + + [Test] + public void UsesExplicitTarget() + { + var cmd = new EventAction(this.target, this.eventInfo.EventHandlerType, "DoSomething", ActionUnavailableBehaviour.Throw, ActionUnavailableBehaviour.Throw); + cmd.GetDelegate().DynamicInvoke(null, null); + Assert.True(this.target.DoSomethingCalled); + } } } diff --git a/StyletUnitTests/StyletUnitTests.csproj b/StyletUnitTests/StyletUnitTests.csproj index a4f81e9..16e0bc9 100644 --- a/StyletUnitTests/StyletUnitTests.csproj +++ b/StyletUnitTests/StyletUnitTests.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows + net472;net5.0-windows true false