diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..87ed07e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.cs eol=crlf +*.xaml eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b423ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +#Visual Studio files +*.[Oo]bj +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.[Cc]ache +*.ilk +*.log +*.lib +*.sbr +*.sdf +*.opensdf +*.unsuccessfulbuild +ipch/ +obj/ +[Bb]in +[Dd]ebug*/ +[Rr]elease*/ + +#Project files +[Bb]uild/ + +#NuGet +packages/ +*.nupkg + +#Installer +installer/Output + +*.gjq +*.tmp diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe new file mode 100644 index 0000000..3ffdd33 Binary files /dev/null and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets new file mode 100644 index 0000000..2c3545b --- /dev/null +++ b/.nuget/NuGet.targets @@ -0,0 +1,151 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + packages.$(MSBuildProjectName.Replace(' ', '_')).config + + + + + + $(PackagesProjectConfig) + + + + + packages.config + + + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 $(NuGetExePath) + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..35e1135 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/NuGet/CHANGELOG.txt b/NuGet/CHANGELOG.txt new file mode 100644 index 0000000..b33aa2b --- /dev/null +++ b/NuGet/CHANGELOG.txt @@ -0,0 +1,7 @@ +Stylet Changelog +================ + +v0.9.1 +------ + +Initial release diff --git a/NuGet/NuGet.exe b/NuGet/NuGet.exe new file mode 100644 index 0000000..3ffdd33 Binary files /dev/null and b/NuGet/NuGet.exe differ diff --git a/NuGet/Stylet.nuspec b/NuGet/Stylet.nuspec new file mode 100644 index 0000000..777159f --- /dev/null +++ b/NuGet/Stylet.nuspec @@ -0,0 +1,25 @@ + + + + Stylet + 0.9.1 + Stylet + Antony Male + Antony Male + http://github.com/canton7/Stylet/blob/master/LICENSE.txt + http://github.com/canton7/Stylet + https://raw.githubusercontent.com/canton7/Stylet/master/StyletIcon.png + false + A very lightweight but powerful ViewModel-First MVVM framework for WPF, inspired by Caliburn.Micro. Comes with its own IoC container. + Copyright 2014 Antony Male + WPF MVVM ViewModel Screen Conductor ViewModel-First Model-View-ViewModel UI + + + + + + + + + + diff --git a/NuGet/content/net45/StyletReadme.txt b/NuGet/content/net45/StyletReadme.txt new file mode 100644 index 0000000..795f364 --- /dev/null +++ b/NuGet/content/net45/StyletReadme.txt @@ -0,0 +1,3 @@ +Welcome to Stylet! Stylet is a small but powerful MVVM framework, which supports a ViewModel-First approach. + +Please read the documentation at https://github.com/canton7/Stylet/wiki diff --git a/NuGet/tools/install.ps1 b/NuGet/tools/install.ps1 new file mode 100644 index 0000000..f6a7499 --- /dev/null +++ b/NuGet/tools/install.ps1 @@ -0,0 +1,8 @@ +param($installPath, $toolsPath, $package, $project) + +Write-Host "Hello" +Write-Host $project.FullName +$path = [System.IO.Path] +$readmefile = $path::Combine($path::GetDirectoryName($project.FullName), "StyletReadme.txt") +$DTE.ItemOperations.OpenFile($readmefile) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b129ff --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +StyletIoC +========= + +This project is still in development. + +[The wiki](https://github.com/canton7/Stylet/wiki/_pages) is the official (incomplete) documentation source. diff --git a/Samples/.nuget/NuGet.Config b/Samples/.nuget/NuGet.Config new file mode 100644 index 0000000..67f8ea0 --- /dev/null +++ b/Samples/.nuget/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/.nuget/NuGet.exe b/Samples/.nuget/NuGet.exe new file mode 100644 index 0000000..9cba6ed Binary files /dev/null and b/Samples/.nuget/NuGet.exe differ diff --git a/Samples/.nuget/NuGet.targets b/Samples/.nuget/NuGet.targets new file mode 100644 index 0000000..2c3545b --- /dev/null +++ b/Samples/.nuget/NuGet.targets @@ -0,0 +1,151 @@ + + + + $(MSBuildProjectDirectory)\..\ + + + false + + + false + + + true + + + false + + + + + + + + + + + $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) + + + + + $(SolutionDir).nuget + + + + packages.$(MSBuildProjectName.Replace(' ', '_')).config + + + + + + $(PackagesProjectConfig) + + + + + packages.config + + + + + + + $(NuGetToolsPath)\NuGet.exe + @(PackageSource) + + "$(NuGetExePath)" + mono --runtime=v4.0.30319 $(NuGetExePath) + + $(TargetDir.Trim('\\')) + + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + + + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols + + + + RestorePackages; + $(BuildDependsOn); + + + + + $(BuildDependsOn); + BuildPackage; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.Hello/App.config b/Samples/Stylet.Samples.Hello/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.Hello/App.xaml b/Samples/Stylet.Samples.Hello/App.xaml new file mode 100644 index 0000000..80e41ce --- /dev/null +++ b/Samples/Stylet.Samples.Hello/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.Hello/HelloBootstrapper.cs b/Samples/Stylet.Samples.Hello/HelloBootstrapper.cs new file mode 100644 index 0000000..ca0c352 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/HelloBootstrapper.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.Hello +{ + class HelloBootstrapper : Bootstrapper + { + } +} diff --git a/Samples/Stylet.Samples.Hello/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.Hello/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ac35c77 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/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.Hello")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.Hello")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.Hello/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.Hello/Properties/Resources.Designer.cs new file mode 100644 index 0000000..eb301ed --- /dev/null +++ b/Samples/Stylet.Samples.Hello/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.Hello.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.Hello.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.Hello/Properties/Resources.resx b/Samples/Stylet.Samples.Hello/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.Hello/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.Hello/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.Hello/Properties/Settings.Designer.cs new file mode 100644 index 0000000..6841459 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.Hello.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.Hello/Properties/Settings.settings b/Samples/Stylet.Samples.Hello/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.Hello/ShellView.xaml b/Samples/Stylet.Samples.Hello/ShellView.xaml new file mode 100644 index 0000000..e944ad2 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/ShellView.xaml @@ -0,0 +1,10 @@ + + + + + + diff --git a/Samples/Stylet.Samples.Hello/ShellViewModel.cs b/Samples/Stylet.Samples.Hello/ShellViewModel.cs new file mode 100644 index 0000000..df3b723 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/ShellViewModel.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet.Samples.Hello +{ + class ShellViewModel : Screen + { + private string _name; + public string Name + { + get { return this._name; } + set { SetAndNotify(ref this._name, value); this.NotifyOfPropertyChange(() => this.CanSayHello); } + } + + public ShellViewModel() + { + this.DisplayName = "Hello, Stylet"; + } + + public bool CanSayHello + { + get { return !String.IsNullOrEmpty(this.Name); } + } + public void SayHello() + { + MessageBox.Show(String.Format("Hello, {0}", this.Name)); // Don't do this + } + } +} diff --git a/Samples/Stylet.Samples.Hello/Stylet.Samples.Hello.csproj b/Samples/Stylet.Samples.Hello/Stylet.Samples.Hello.csproj new file mode 100644 index 0000000..c4edc72 --- /dev/null +++ b/Samples/Stylet.Samples.Hello/Stylet.Samples.Hello.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {6C7FBB21-52AC-4333-A42A-9F5E9D048621} + WinExe + Properties + Stylet.Samples.Hello + Stylet.Samples.Hello + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + Designer + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.HelloDialog/App.config b/Samples/Stylet.Samples.HelloDialog/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.HelloDialog/App.xaml b/Samples/Stylet.Samples.HelloDialog/App.xaml new file mode 100644 index 0000000..4658d8d --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.HelloDialog/Bootstrapper.cs b/Samples/Stylet.Samples.HelloDialog/Bootstrapper.cs new file mode 100644 index 0000000..5bf13d5 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Bootstrapper.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.HelloDialog +{ + public class Bootstrapper : Bootstrapper + { + protected override void ConfigureIoC(StyletIoC.IStyletIoCBuilder builder) + { + base.ConfigureIoC(builder); + + builder.Bind().ToAbstractFactory(); + } + } +} diff --git a/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml b/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml new file mode 100644 index 0000000..713f4d2 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Dialog1View.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs b/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs new file mode 100644 index 0000000..85d412b --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Dialog1ViewModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.HelloDialog +{ + public class Dialog1ViewModel : Screen + { + public string Name { get; set; } + + public Dialog1ViewModel() + { + this.DisplayName = "I'm Dialog 1"; + } + + public void Close() + { + this.TryClose(true); + } + } +} diff --git a/Samples/Stylet.Samples.HelloDialog/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.HelloDialog/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d907e2a --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/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.HelloDialog")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.HelloDialog")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.HelloDialog/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.HelloDialog/Properties/Resources.Designer.cs new file mode 100644 index 0000000..16ace46 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.HelloDialog.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.HelloDialog.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.HelloDialog/Properties/Resources.resx b/Samples/Stylet.Samples.HelloDialog/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/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.HelloDialog/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.HelloDialog/Properties/Settings.Designer.cs new file mode 100644 index 0000000..15089d9 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.HelloDialog.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.HelloDialog/Properties/Settings.settings b/Samples/Stylet.Samples.HelloDialog/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.HelloDialog/ShellView.xaml b/Samples/Stylet.Samples.HelloDialog/ShellView.xaml new file mode 100644 index 0000000..8adad9f --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/ShellView.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs b/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs new file mode 100644 index 0000000..a5ba9e2 --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/ShellViewModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.HelloDialog +{ + public class ShellViewModel : Screen + { + private IWindowManager windowManager; + private IDialogFactory dialogFactory; + + private string _nameString; + public string NameString + { + get { return this._nameString; } + set { SetAndNotify(ref _nameString, value); } + } + + public ShellViewModel(IWindowManager windowManager, IDialogFactory dialogFactory) + { + this.DisplayName = "Hello Dialog"; + + this.windowManager = windowManager; + this.dialogFactory = dialogFactory; + + this.NameString = "Click the button to show the dialog"; + } + + public void ShowDialog() + { + var dialogVm = this.dialogFactory.CreateDialog1(); + if (this.windowManager.ShowDialog(dialogVm).GetValueOrDefault()) + this.NameString = String.Format("Your name is {0}", dialogVm.Name); + else + this.NameString = "Dialog cancelled"; + } + } + + public interface IDialogFactory + { + Dialog1ViewModel CreateDialog1(); + } +} diff --git a/Samples/Stylet.Samples.HelloDialog/Stylet.Samples.HelloDialog.csproj b/Samples/Stylet.Samples.HelloDialog/Stylet.Samples.HelloDialog.csproj new file mode 100644 index 0000000..606c84f --- /dev/null +++ b/Samples/Stylet.Samples.HelloDialog/Stylet.Samples.HelloDialog.csproj @@ -0,0 +1,113 @@ + + + + + Debug + AnyCPU + {F6DD6F38-40A3-4EC1-B342-0C2BCCF0DD44} + WinExe + Properties + Stylet.Samples.HelloDialog + Stylet.Samples.HelloDialog + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.MasterDetail/App.config b/Samples/Stylet.Samples.MasterDetail/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.MasterDetail/App.xaml b/Samples/Stylet.Samples.MasterDetail/App.xaml new file mode 100644 index 0000000..c48900c --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.MasterDetail/Bootstrapper.cs b/Samples/Stylet.Samples.MasterDetail/Bootstrapper.cs new file mode 100644 index 0000000..4dbc01b --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/Bootstrapper.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.MasterDetail +{ + public class Bootstrapper : Bootstrapper + { + } +} diff --git a/Samples/Stylet.Samples.MasterDetail/EmployeeModel.cs b/Samples/Stylet.Samples.MasterDetail/EmployeeModel.cs new file mode 100644 index 0000000..d8f36b9 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/EmployeeModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.MasterDetail +{ + public class EmployeeModel + { + public string Name { get; set; } + } +} diff --git a/Samples/Stylet.Samples.MasterDetail/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.MasterDetail/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..36d8397 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/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.MasterDetail")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.MasterDetail")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.MasterDetail/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.MasterDetail/Properties/Resources.Designer.cs new file mode 100644 index 0000000..7e1f8d8 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.MasterDetail.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.MasterDetail.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.MasterDetail/Properties/Resources.resx b/Samples/Stylet.Samples.MasterDetail/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/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.MasterDetail/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.MasterDetail/Properties/Settings.Designer.cs new file mode 100644 index 0000000..7c3053b --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.MasterDetail.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.MasterDetail/Properties/Settings.settings b/Samples/Stylet.Samples.MasterDetail/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.MasterDetail/ShellView.xaml b/Samples/Stylet.Samples.MasterDetail/ShellView.xaml new file mode 100644 index 0000000..c4f1075 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/ShellView.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + Name: + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.MasterDetail/ShellViewModel.cs b/Samples/Stylet.Samples.MasterDetail/ShellViewModel.cs new file mode 100644 index 0000000..36730b3 --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/ShellViewModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.MasterDetail +{ + public class ShellViewModel : Screen + { + public IObservableCollection Employees { get; private set; } + + private EmployeeModel _selectedEmployee; + public EmployeeModel SelectedEmployee + { + get { return this._selectedEmployee; } + set { SetAndNotify(ref this._selectedEmployee, value) ;} + } + + public ShellViewModel() + { + this.DisplayName = "Master-Detail"; + + this.Employees = new BindableCollection(); + + this.Employees.Add(new EmployeeModel() { Name = "Fred" }); + this.Employees.Add(new EmployeeModel() { Name = "Bob" }); + + this.SelectedEmployee = this.Employees.FirstOrDefault(); + } + + public void AddEmployee() + { + this.Employees.Add(new EmployeeModel() { Name = "Unnamed" }); + } + + public void RemoveEmployee(EmployeeModel item) + { + this.Employees.Remove(item); + } + } +} diff --git a/Samples/Stylet.Samples.MasterDetail/Stylet.Samples.MasterDetail.csproj b/Samples/Stylet.Samples.MasterDetail/Stylet.Samples.MasterDetail.csproj new file mode 100644 index 0000000..479e22f --- /dev/null +++ b/Samples/Stylet.Samples.MasterDetail/Stylet.Samples.MasterDetail.csproj @@ -0,0 +1,105 @@ + + + + + Debug + AnyCPU + {A281DFF2-125E-4412-8927-0F09EEFC5AD1} + WinExe + Properties + Stylet.Samples.MasterDetail + Stylet.Samples.MasterDetail + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + Designer + MSBuild:Compile + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.OverridingViewManager/App.config b/Samples/Stylet.Samples.OverridingViewManager/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.OverridingViewManager/App.xaml b/Samples/Stylet.Samples.OverridingViewManager/App.xaml new file mode 100644 index 0000000..aa2422b --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.OverridingViewManager/Bootstrapper.cs b/Samples/Stylet.Samples.OverridingViewManager/Bootstrapper.cs new file mode 100644 index 0000000..c0903fb --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/Bootstrapper.cs @@ -0,0 +1,19 @@ +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.OverridingViewManager +{ + public class Bootstrapper : Bootstrapper + { + protected override void ConfigureIoC(IStyletIoCBuilder builder) + { + base.ConfigureIoC(builder); + + builder.Bind().To().InSingletonScope(); + } + } +} diff --git a/Samples/Stylet.Samples.OverridingViewManager/CustomViewManager.cs b/Samples/Stylet.Samples.OverridingViewManager/CustomViewManager.cs new file mode 100644 index 0000000..aee9912 --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/CustomViewManager.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet.Samples.OverridingViewManager +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + sealed class ViewModelAttribute : Attribute + { + readonly Type viewModel; + + public ViewModelAttribute(Type viewModel) + { + this.viewModel = viewModel; + } + + public Type ViewModel + { + get { return viewModel; } + } + } + + public class CustomViewManager : ViewManager + { + // Dictionary of ViewModel type -> View type + private Dictionary viewModelToViewMapping; + + public CustomViewManager() + { + var mappings = from type in AssemblySource.Assemblies.SelectMany(x => x.GetTypes()) + let attributes = (ViewModelAttribute[])type.GetCustomAttributes(typeof(ViewModelAttribute), false) + where attributes.Length == 1 && typeof(UIElement).IsAssignableFrom(type) + select new { View = type, ViewModel = attributes[0].ViewModel }; + + this.viewModelToViewMapping = mappings.ToDictionary(x => x.ViewModel, x => x.View); + } + + public override UIElement CreateViewForModel(object model) + { + Type viewType; + if (!this.viewModelToViewMapping.TryGetValue(model.GetType(), out viewType)) + throw new Exception(String.Format("Could not find View for ViewModel {0}", model.GetType().Name)); + return (UIElement)IoC.GetInstance(viewType, null); + } + } +} diff --git a/Samples/Stylet.Samples.OverridingViewManager/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.OverridingViewManager/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2a6e1ec --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/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.OverridingViewManager")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.OverridingViewManager")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.OverridingViewManager/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.OverridingViewManager/Properties/Resources.Designer.cs new file mode 100644 index 0000000..5c00f16 --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.OverridingViewManager.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.OverridingViewManager.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.OverridingViewManager/Properties/Resources.resx b/Samples/Stylet.Samples.OverridingViewManager/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/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.OverridingViewManager/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.OverridingViewManager/Properties/Settings.Designer.cs new file mode 100644 index 0000000..ea9af1f --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.OverridingViewManager.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.OverridingViewManager/Properties/Settings.settings b/Samples/Stylet.Samples.OverridingViewManager/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml b/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml new file mode 100644 index 0000000..6aff50f --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml @@ -0,0 +1,8 @@ + + + Hello, World + + diff --git a/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml.cs b/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml.cs new file mode 100644 index 0000000..5e5e80d --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/ShellView.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace Stylet.Samples.OverridingViewManager +{ + /// + /// Interaction logic for ShellView.xaml + /// + [ViewModel(typeof(ShellViewModel))] + public partial class ShellView : Window + { + public ShellView() + { + InitializeComponent(); + } + } +} diff --git a/Samples/Stylet.Samples.OverridingViewManager/ShellViewModel.cs b/Samples/Stylet.Samples.OverridingViewManager/ShellViewModel.cs new file mode 100644 index 0000000..1b22c0b --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/ShellViewModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.OverridingViewManager +{ + public class ShellViewModel + { + } +} diff --git a/Samples/Stylet.Samples.OverridingViewManager/Stylet.Samples.OverridingViewManager.csproj b/Samples/Stylet.Samples.OverridingViewManager/Stylet.Samples.OverridingViewManager.csproj new file mode 100644 index 0000000..5a51730 --- /dev/null +++ b/Samples/Stylet.Samples.OverridingViewManager/Stylet.Samples.OverridingViewManager.csproj @@ -0,0 +1,110 @@ + + + + + Debug + AnyCPU + {2F7D7EF3-730A-45E3-93CA-7C5031250246} + WinExe + Properties + Stylet.Samples.OverridingViewManager + Stylet.Samples.OverridingViewManager + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ShellView.xaml + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + Designer + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.RedditBrowser/App.config b/Samples/Stylet.Samples.RedditBrowser/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.RedditBrowser/App.xaml b/Samples/Stylet.Samples.RedditBrowser/App.xaml new file mode 100644 index 0000000..1e94178 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Bootstrapper.cs b/Samples/Stylet.Samples.RedditBrowser/Bootstrapper.cs new file mode 100644 index 0000000..e63514e --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Bootstrapper.cs @@ -0,0 +1,22 @@ +using Stylet.Samples.RedditBrowser.Pages; +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser +{ + public class Bootstrapper : Bootstrapper + { + protected override void ConfigureIoC(StyletIoC.IStyletIoCBuilder builder) + { + base.ConfigureIoC(builder); + + builder.Bind().To().InSingletonScope(); + builder.Bind().ToAbstractFactory(); + builder.Bind().ToAbstractFactory(); + } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Events/OpenSubredditEvent.cs b/Samples/Stylet.Samples.RedditBrowser/Events/OpenSubredditEvent.cs new file mode 100644 index 0000000..d3aa2ea --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Events/OpenSubredditEvent.cs @@ -0,0 +1,15 @@ +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Events +{ + public class OpenSubredditEvent + { + public string Subreddit { get; set; } + public SortMode SortMode { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/FodyWeavers.xml b/Samples/Stylet.Samples.RedditBrowser/FodyWeavers.xml new file mode 100644 index 0000000..7369928 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsView.xaml b/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsView.xaml new file mode 100644 index 0000000..5f99960 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsView.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsViewModel.cs b/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsViewModel.cs new file mode 100644 index 0000000..cdfd08c --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/PostCommentsViewModel.cs @@ -0,0 +1,33 @@ +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Pages +{ + public class PostCommentsViewModel : Screen + { + private IRedditClient client; + + public string Subreddit { get; set; } + public string PostId36 { get; set; } + public CommentCollection CommentCollection { get; private set; } + + public PostCommentsViewModel(IRedditClient client) + { + this.client = client; + } + + protected override async void OnInitialActivate() + { + this.CommentCollection = await this.client.GetPostComments(this.Subreddit, this.PostId36); + } + + public void GoBack() + { + this.TryClose(); + } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/PostsView.xaml b/Samples/Stylet.Samples.RedditBrowser/Pages/PostsView.xaml new file mode 100644 index 0000000..9b49bad --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/PostsView.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/PostsViewModel.cs b/Samples/Stylet.Samples.RedditBrowser/Pages/PostsViewModel.cs new file mode 100644 index 0000000..3fd7da1 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/PostsViewModel.cs @@ -0,0 +1,94 @@ +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Pages +{ + public class PostsViewModel : Screen + { + public event EventHandler PostCommentsOpened; + + private IRedditClient client; + + public string Subreddit { get; set; } + public SortMode SortMode { get; set; } + public PostCollection PostCollection { get; private set; } + public Post SelectedPost { get; set; } + + public PostsViewModel(IRedditClient client) + { + this.client = client; + } + + protected override async void OnInitialActivate() + { + // Really un-advanced - just close + try + { + this.PostCollection = await this.client.GetPostsAsync(this.Subreddit, this.SortMode); + } + catch (Exception) + { + this.TryClose(); + } + } + + public void Close() + { + this.TryClose(); + } + + public bool CanNext + { + get { return this.PostCollection != null && this.PostCollection.HasNext; } + } + public async void Next() + { + this.PostCollection = await this.PostCollection.NextAsync(); + } + + public bool CanPrev + { + get { return this.PostCollection != null && this.PostCollection.HasPrev; } + } + public async void Prev() + { + this.PostCollection = await this.PostCollection.PrevAsync(); + } + + public bool CanOpenSelected + { + get { return this.SelectedPost != null; } + } + public void OpenSelected() + { + if (this.SelectedPost == null) + return; + + Process.Start(this.SelectedPost.LinkUrl); + } + + public bool CanOpenSelectedComments + { + get { return this.SelectedPost != null && this.SelectedPost.NumComments > 0; } + } + public void OpenSelectedComments() + { + if (this.SelectedPost == null) + return; + + var handler = this.PostCommentsOpened; + if (handler != null) + handler(this, new PostCommentsOpenedEventArgs() { PostId36 = this.SelectedPost.Id36 }); + } + } + + public class PostCommentsOpenedEventArgs : EventArgs + { + public string PostId36 { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/ShellView.xaml b/Samples/Stylet.Samples.RedditBrowser/Pages/ShellView.xaml new file mode 100644 index 0000000..996c9fe --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/ShellView.xaml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/ShellViewModel.cs b/Samples/Stylet.Samples.RedditBrowser/Pages/ShellViewModel.cs new file mode 100644 index 0000000..898a19c --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/ShellViewModel.cs @@ -0,0 +1,39 @@ +using Stylet.Samples.RedditBrowser.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Pages +{ + public class ShellViewModel : Conductor.Collections.OneActive, IHandle + { + private ISubredditViewModelFactory subredditViewModelFactory; + + public TaskbarViewModel Taskbar { get; private set; } + + public ShellViewModel(IEventAggregator events, TaskbarViewModel taskbarViewModel, ISubredditViewModelFactory subredditViewModelFactory) + { + this.DisplayName = "Reddit Browser"; + + this.Taskbar = taskbarViewModel; + this.subredditViewModelFactory = subredditViewModelFactory; + + events.Subscribe(this); + } + + public void Handle(OpenSubredditEvent message) + { + var item = this.subredditViewModelFactory.CreateSubredditViewModel(); + item.Subreddit = message.Subreddit; + item.SortMode = message.SortMode; + this.ActivateItem(item); + } + } + + public interface ISubredditViewModelFactory + { + SubredditViewModel CreateSubredditViewModel(); + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditView.xaml b/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditView.xaml new file mode 100644 index 0000000..da432ac --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditView.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditViewModel.cs b/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditViewModel.cs new file mode 100644 index 0000000..5a2a2d3 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/SubredditViewModel.cs @@ -0,0 +1,48 @@ +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Pages +{ + public class SubredditViewModel : Conductor.Collections.Navigation + { + private IPostCommentsViewModelFactory postCommentsViewModelFactory; + + public string Subreddit { get; set; } + public SortMode SortMode { get; set; } + private PostsViewModel posts; + + public SubredditViewModel(PostsViewModel posts, IPostCommentsViewModelFactory postCommentsViewModelFactory) + { + this.posts = posts; + this.postCommentsViewModelFactory = postCommentsViewModelFactory; + + this.posts.PostCommentsOpened += (o, e) => this.OpenPostCommands(e.PostId36); + this.posts.Closed += (o, e) => this.TryClose(); + } + + protected override void OnInitialActivate() + { + this.DisplayName = String.Format("/r/{0}", this.Subreddit); + this.posts.Subreddit = this.Subreddit; + this.posts.SortMode = this.SortMode; + + this.ActivateItem(this.posts); + } + + private void OpenPostCommands(string postId36) + { + var item = this.postCommentsViewModelFactory.CreatePostCommentsViewModel(); + item.PostId36 = postId36; + this.ActivateItem(item); + } + } + + public interface IPostCommentsViewModelFactory + { + PostCommentsViewModel CreatePostCommentsViewModel(); + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarView.xaml b/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarView.xaml new file mode 100644 index 0000000..d2b5cf9 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarView.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarViewModel.cs b/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarViewModel.cs new file mode 100644 index 0000000..2d9ca0b --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Pages/TaskbarViewModel.cs @@ -0,0 +1,32 @@ +using Stylet.Samples.RedditBrowser.Events; +using Stylet.Samples.RedditBrowser.RedditApi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.Pages +{ + public class TaskbarViewModel : Screen + { + private IEventAggregator events; + + public string Subreddit { get; set; } + + public IEnumerable SortModes { get; private set; } + public SortMode SelectedSortMode { get; set; } + + public TaskbarViewModel(IEventAggregator events) + { + this.events = events; + this.SortModes = SortMode.AllModes; + this.SelectedSortMode = SortMode.Hot; + } + + public void Open() + { + this.events.Publish(new OpenSubredditEvent() { Subreddit = this.Subreddit, SortMode = this.SelectedSortMode }); + } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.RedditBrowser/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..bf98775 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/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.RedditBrowser")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.RedditBrowser")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.RedditBrowser/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.RedditBrowser/Properties/Resources.Designer.cs new file mode 100644 index 0000000..d6a0645 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.RedditBrowser.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.RedditBrowser.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.RedditBrowser/Properties/Resources.resx b/Samples/Stylet.Samples.RedditBrowser/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/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.RedditBrowser/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.RedditBrowser/Properties/Settings.Designer.cs new file mode 100644 index 0000000..38e51b5 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.RedditBrowser.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.RedditBrowser/Properties/Settings.settings b/Samples/Stylet.Samples.RedditBrowser/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.RedditBrowser/RedditApi/CommentCollection.cs b/Samples/Stylet.Samples.RedditBrowser/RedditApi/CommentCollection.cs new file mode 100644 index 0000000..0695756 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/RedditApi/CommentCollection.cs @@ -0,0 +1,51 @@ +using Spring.Rest.Client; +using Stylet.Samples.RedditBrowser.RedditApi.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.RedditApi +{ + public class CommentCollection + { + private RestTemplate template; + private string subreddit; + private string postId; + + public IReadOnlyList Comments { get; private set; } + + public CommentCollection(RestTemplate template, string subreddit, string postId) + { + this.template = template; + this.subreddit = subreddit; + this.postId = postId; + } + + public async Task LoadAsync() + { + var comments = await this.template.GetForObjectAsync>("/r/{subreddit}/comments/{postid}.json", this.subreddit, this.postId); + this.Comments = comments.SelectMany(x => x.Data.Children).Where(x => x.Kind == "t1").Select(x => this.ContractToComment(x.Data)).ToList(); + } + + private Comment ContractToComment(CommentData data) + { + var comment = new Comment() + { + Body = data.Body, + Author = data.Author + }; + if (data.Replies != null && data.Replies.Data != null) + comment.Replies = data.Replies.Data.Children.Select(x => this.ContractToComment(x.Data)).ToList(); + return comment; + } + } + + public class Comment + { + public string Body { get; set; } + public string Author { get; set; } + public List Replies { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/CommentsResponse.cs b/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/CommentsResponse.cs new file mode 100644 index 0000000..edf8e52 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/CommentsResponse.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.RedditApi.Contracts +{ + [DataContract] + public class CommentsResponse + { + [DataMember(Name = "kind")] + public string Kind { get; set; } + + [DataMember(Name = "data")] + public CommentsResponseData Data { get; set; } + } + + [DataContract] + public class CommentsResponseData + { + [DataMember(Name = "before")] + public string Before { get; set; } + + [DataMember(Name = "after")] + public string After { get; set; } + + [DataMember(Name = "children")] + public List Children { get; set; } + } + + [DataContract] + public class CommentListing + { + [DataMember(Name = "kind")] + public string Kind { get; set; } + + [DataMember(Name = "data")] + public CommentData Data { get; set; } + } + + [DataContract] + public class CommentData + { + [DataMember(Name = "body")] + public string Body { get; set; } + + [DataMember(Name = "replies")] + public CommentsResponse Replies { get; set; } + + [DataMember(Name = "author")] + public string Author { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/PostsResponse.cs b/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/PostsResponse.cs new file mode 100644 index 0000000..2fc3f69 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/RedditApi/Contracts/PostsResponse.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.RedditApi.Contracts +{ + [DataContract] + public class PostsResponse + { + [DataMember(Name = "data")] + public PostsResponseData Data { get; set; } + } + + [DataContract] + public class PostsResponseData + { + [DataMember(Name = "before")] + public string Before { get; set; } + + [DataMember(Name = "after")] + public string After { get; set; } + + [DataMember(Name = "children")] + public List Children { get; set; } + } + + [DataContract] + public class Post + { + [DataMember(Name = "data")] + public PostData Data { get; set; } + } + + [DataContract] + public class PostData + { + [DataMember(Name = "title")] + public string Title { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "num_comments")] + public int NumComments { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/RedditApi/PostCollection.cs b/Samples/Stylet.Samples.RedditBrowser/RedditApi/PostCollection.cs new file mode 100644 index 0000000..0a3f5c8 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/RedditApi/PostCollection.cs @@ -0,0 +1,89 @@ +using Spring.Rest.Client; +using Stylet.Samples.RedditBrowser.RedditApi.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.RedditApi +{ + public class PostCollection + { + private RestTemplate template; + private string subreddit; + private string sortMode; + private string after; + private string before; + /// + /// The number of items we've seen in this listing, *INCLUDING* the current posts + /// + private int count; + + public IReadOnlyList Posts { get; private set; } + + public bool HasNext + { + get { return this.after != null; } + } + public bool HasPrev + { + get { return this.before != null; } + } + + public PostCollection(RestTemplate template, string subreddit, string sortMode) + : this(template, subreddit, sortMode, 0) { } + + private PostCollection(RestTemplate template, string subreddit, string sortMode, int count) + { + this.template = template; + this.subreddit = subreddit; + this.sortMode = sortMode; + this.count = count; + } + + public async Task LoadAsync() + { + var posts = await this.template.GetForObjectAsync("/r/{subreddit}/{mode}.json", this.subreddit, this.sortMode); + this.LoadPostsResponse(posts); + this.count = this.Posts.Count; + } + + private void LoadPostsResponse(PostsResponse posts) + { + this.Posts = posts.Data.Children.Select(x => new Post() + { + Title = x.Data.Title, + LinkUrl = x.Data.Url, + Id36 = x.Data.Id, + NumComments = x.Data.NumComments, + }).ToList(); + this.after = posts.Data.After; + this.before = posts.Data.Before; + } + + public async Task NextAsync() + { + var posts = await this.template.GetForObjectAsync("/r/{subreddit}/{mode}.json?after={after}&count={count}", this.subreddit, this.sortMode, this.after, this.count); + var result = new PostCollection(this.template, this.subreddit, this.sortMode, this.count + posts.Data.Children.Count); + result.LoadPostsResponse(posts); + return result; + } + + public async Task PrevAsync() + { + var posts = await this.template.GetForObjectAsync("/r/{subreddit}/{mode}.json?before={before}&count={count}", this.subreddit, this.sortMode, this.before, this.count + 1 - this.Posts.Count); + var result = new PostCollection(this.template, this.subreddit, this.sortMode, this.count - posts.Data.Children.Count); + result.LoadPostsResponse(posts); + return result; + } + } + + public class Post + { + public string Title { get; set; } + public string LinkUrl { get; set; } + public string Id36 { get; set; } + public int NumComments { get; set; } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/RedditApi/RedditClient.cs b/Samples/Stylet.Samples.RedditBrowser/RedditApi/RedditClient.cs new file mode 100644 index 0000000..437f4cb --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/RedditApi/RedditClient.cs @@ -0,0 +1,62 @@ +using Spring.Http.Converters.Json; +using Spring.Rest.Client; +using Stylet.Samples.RedditBrowser.RedditApi.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.RedditBrowser.RedditApi +{ + public interface IRedditClient + { + Task GetPostsAsync(string subreddit, SortMode sortMode); + Task GetPostComments(string subreddit, string postId36); + } + + public class RedditClient : IRedditClient + { + private RestTemplate template; + + public RedditClient() + { + this.template = new RestTemplate("http://reddit.com"); + template.MessageConverters.Add(new DataContractJsonHttpMessageConverter()); + } + + public async Task GetPostsAsync(string subreddit, SortMode sortMode) + { + var postCollection = new PostCollection(this.template, subreddit, sortMode.Mode); + await postCollection.LoadAsync(); + return postCollection; + } + + public async Task GetPostComments(string subreddit, string postId36) + { + var commentCollection = new CommentCollection(this.template, subreddit, postId36); + await commentCollection.LoadAsync(); + return commentCollection; + } + } + + public struct SortMode + { + public static SortMode New = new SortMode("new", "New"); + public static SortMode Hot = new SortMode("hot", "Hot"); + public static SortMode Top = new SortMode("top", "Top"); + + public static IEnumerable AllModes + { + get { return new[] { New, Hot, Top }; } + } + + public string Name { get; private set; } + public string Mode { get; private set; } + private SortMode(string mode, string name) : this() + { + this.Mode = mode; + this.Name = name; + } + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/Stylet.Samples.RedditBrowser.csproj b/Samples/Stylet.Samples.RedditBrowser/Stylet.Samples.RedditBrowser.csproj new file mode 100644 index 0000000..89290f1 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/Stylet.Samples.RedditBrowser.csproj @@ -0,0 +1,160 @@ + + + + + Debug + AnyCPU + {72B1C6E4-1293-47DD-BEFD-FA2E782BDBDA} + WinExe + Properties + Stylet.Samples.RedditBrowser + Stylet.Samples.RedditBrowser + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + ..\ + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\Common.Logging.2.0.0\lib\2.0\Common.Logging.dll + + + ..\packages\PropertyChanged.Fody.1.48.0.0\Lib\NET35\PropertyChanged.dll + False + + + False + ..\packages\Spring.Rest.1.1.1\lib\net40-client\Spring.Rest.dll + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + + + + + + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.RedditBrowser/UserControls/StretchingTreeView.cs b/Samples/Stylet.Samples.RedditBrowser/UserControls/StretchingTreeView.cs new file mode 100644 index 0000000..6d701ec --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/UserControls/StretchingTreeView.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; + +namespace Stylet.Samples.RedditBrowser.UserControls +{ + // http://blogs.msdn.com/b/jpricket/archive/2008/08/05/wpf-a-stretching-treeview.aspx + public class StretchingTreeView : TreeView + { + protected override DependencyObject GetContainerForItemOverride() + { + return new StretchingTreeViewItem(); + } + + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is StretchingTreeViewItem; + } + } + + public class StretchingTreeViewItem : TreeViewItem + { + public StretchingTreeViewItem() + { + this.Loaded += new RoutedEventHandler(StretchingTreeViewItem_Loaded); + } + + private void StretchingTreeViewItem_Loaded(object sender, RoutedEventArgs e) + { + // The purpose of this code is to stretch the Header Content all the way accross the TreeView. + if (this.VisualChildrenCount > 0) + { + Grid grid = this.GetVisualChild(0) as Grid; + if (grid != null && grid.ColumnDefinitions.Count == 3) + { + // Remove the middle column which is set to Auto and let it get replaced with the + // last column that is set to Star. + grid.ColumnDefinitions.RemoveAt(1); + } + } + } + + protected override DependencyObject GetContainerForItemOverride() + { + return new StretchingTreeViewItem(); + } + + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is StretchingTreeViewItem; + } + + // http://stackoverflow.com/a/2957734/1086121 + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (Template != null) + { + var btn = Template.FindName("Expander", this) as ToggleButton; + if (btn != null) + btn.VerticalAlignment = this.ToggleButtonVerticalAlignment; + } + } + + + + public VerticalAlignment ToggleButtonVerticalAlignment + { + get { return (VerticalAlignment)GetValue(ToggleButtonVerticalAlignmentProperty); } + set { SetValue(ToggleButtonVerticalAlignmentProperty, value); } + } + + // Using a DependencyProperty as the backing store for ToggleButtonVerticalAlignment. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ToggleButtonVerticalAlignmentProperty = + DependencyProperty.Register("ToggleButtonVerticalAlignment", typeof(VerticalAlignment), typeof(StretchingTreeViewItem), new PropertyMetadata(VerticalAlignment.Center)); + + + } +} diff --git a/Samples/Stylet.Samples.RedditBrowser/packages.config b/Samples/Stylet.Samples.RedditBrowser/packages.config new file mode 100644 index 0000000..db9cc43 --- /dev/null +++ b/Samples/Stylet.Samples.RedditBrowser/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.TabNavigation/App.config b/Samples/Stylet.Samples.TabNavigation/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.TabNavigation/App.xaml b/Samples/Stylet.Samples.TabNavigation/App.xaml new file mode 100644 index 0000000..69c10b6 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Samples/Stylet.Samples.TabNavigation/Bootstrapper.cs b/Samples/Stylet.Samples.TabNavigation/Bootstrapper.cs new file mode 100644 index 0000000..d8255b4 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Bootstrapper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet.Samples.TabNavigation +{ + class Bootstrapper : Bootstrapper + { + } +} diff --git a/Samples/Stylet.Samples.TabNavigation/Page1View.xaml b/Samples/Stylet.Samples.TabNavigation/Page1View.xaml new file mode 100644 index 0000000..1f6e742 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Page1View.xaml @@ -0,0 +1,11 @@ + + + This is page 1 + + diff --git a/Samples/Stylet.Samples.TabNavigation/Page1ViewModel.cs b/Samples/Stylet.Samples.TabNavigation/Page1ViewModel.cs new file mode 100644 index 0000000..54e3476 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Page1ViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.TabNavigation +{ + class Page1ViewModel : Screen + { + public Page1ViewModel() + { + this.DisplayName = "Page 1"; + } + } +} diff --git a/Samples/Stylet.Samples.TabNavigation/Page2View.xaml b/Samples/Stylet.Samples.TabNavigation/Page2View.xaml new file mode 100644 index 0000000..3ad7ed0 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Page2View.xaml @@ -0,0 +1,11 @@ + + + This is page 2 + + diff --git a/Samples/Stylet.Samples.TabNavigation/Page2ViewModel.cs b/Samples/Stylet.Samples.TabNavigation/Page2ViewModel.cs new file mode 100644 index 0000000..8f81b44 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Page2ViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.TabNavigation +{ + class Page2ViewModel : Screen + { + public Page2ViewModel() + { + this.DisplayName = "Page 2"; + } + } +} diff --git a/Samples/Stylet.Samples.TabNavigation/Properties/AssemblyInfo.cs b/Samples/Stylet.Samples.TabNavigation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1ed4031 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/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.TabNavigation")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Stylet.Samples.TabNavigation")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[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.TabNavigation/Properties/Resources.Designer.cs b/Samples/Stylet.Samples.TabNavigation/Properties/Resources.Designer.cs new file mode 100644 index 0000000..36865ca --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.TabNavigation.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.TabNavigation.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.TabNavigation/Properties/Resources.resx b/Samples/Stylet.Samples.TabNavigation/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/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.TabNavigation/Properties/Settings.Designer.cs b/Samples/Stylet.Samples.TabNavigation/Properties/Settings.Designer.cs new file mode 100644 index 0000000..02f7865 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Stylet.Samples.TabNavigation.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.TabNavigation/Properties/Settings.settings b/Samples/Stylet.Samples.TabNavigation/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.TabNavigation/ShellView.xaml b/Samples/Stylet.Samples.TabNavigation/ShellView.xaml new file mode 100644 index 0000000..63cf5af --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/ShellView.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Samples/Stylet.Samples.TabNavigation/ShellViewModel.cs b/Samples/Stylet.Samples.TabNavigation/ShellViewModel.cs new file mode 100644 index 0000000..619f43e --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/ShellViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet.Samples.TabNavigation +{ + class ShellViewModel : Conductor.Collections.OneActive + { + public ShellViewModel(Page1ViewModel page1, Page2ViewModel page2) + { + this.ActivateItem(page1); + this.Items.Add(page2); + } + } +} diff --git a/Samples/Stylet.Samples.TabNavigation/Stylet.Samples.TabNavigation.csproj b/Samples/Stylet.Samples.TabNavigation/Stylet.Samples.TabNavigation.csproj new file mode 100644 index 0000000..0b910b0 --- /dev/null +++ b/Samples/Stylet.Samples.TabNavigation/Stylet.Samples.TabNavigation.csproj @@ -0,0 +1,116 @@ + + + + + Debug + AnyCPU + {9A4E2DAD-AE68-4A82-8FA8-407DB74D6FBE} + WinExe + Properties + Stylet.Samples.TabNavigation + Stylet.Samples.TabNavigation + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + \ No newline at end of file diff --git a/Samples/Stylet.Samples.sln b/Samples/Stylet.Samples.sln new file mode 100644 index 0000000..77cd578 --- /dev/null +++ b/Samples/Stylet.Samples.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{C3BAC068-E2C0-4470-9F2E-A1711299BFAE}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Stylet.sln b/Stylet.sln new file mode 100644 index 0000000..96f4ca5 --- /dev/null +++ b/Stylet.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.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}") = "StyletUnitTests", "StyletUnitTests\StyletUnitTests.csproj", "{13AFA20D-CCEA-4A58-920E-4D8816C7296B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{AE3462E7-25F5-4BC8-933A-5D51393E9819}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StyletIntegrationTests", "StyletIntegrationTests\StyletIntegrationTests.csproj", "{C6021258-7CAE-4BC2-A08E-8433019405B7}" +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 + {13AFA20D-CCEA-4A58-920E-4D8816C7296B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13AFA20D-CCEA-4A58-920E-4D8816C7296B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13AFA20D-CCEA-4A58-920E-4D8816C7296B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13AFA20D-CCEA-4A58-920E-4D8816C7296B}.Release|Any CPU.Build.0 = Release|Any CPU + {C6021258-7CAE-4BC2-A08E-8433019405B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6021258-7CAE-4BC2-A08E-8433019405B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6021258-7CAE-4BC2-A08E-8433019405B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6021258-7CAE-4BC2-A08E-8433019405B7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Stylet/AssemblySource.cs b/Stylet/AssemblySource.cs new file mode 100644 index 0000000..d1b975a --- /dev/null +++ b/Stylet/AssemblySource.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Container for the list of assemblies in which Stylet will look for types + /// + public static class AssemblySource + { + /// + /// List of assemblies in which Stylet will look for types, (for autobinding in StyletIoC, and for finding Views). + /// Populated by the Bootstrapper + /// + public static readonly IObservableCollection Assemblies = new BindableCollection(); + } +} diff --git a/Stylet/BindableCollection.cs b/Stylet/BindableCollection.cs new file mode 100644 index 0000000..72f4295 --- /dev/null +++ b/Stylet/BindableCollection.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Represents a collection which is observasble + /// + /// + public interface IObservableCollection : IList, INotifyPropertyChanged, INotifyCollectionChanged, INotifyPropertyChangedDispatcher + { + /// + /// Add a range of items + /// + /// Items to add + void AddRange(IEnumerable items); + + /// + /// Remove a range of items + /// + /// Items to remove + void RemoveRange(IEnumerable items); + } + + /// + /// ObservableCollection subclass which supports AddRange and RemoveRange + /// + /// + public class BindableCollection : ObservableCollection, IObservableCollection + { + private Action _propertyChangedDispatcher = Execute.DefaultPropertyChangedDispatcher; + /// + /// Dispatcher to use when firing events. Defaults to Execute.DefaultPropertyChangedDispatcher + /// + public Action PropertyChangedDispatcher + { + get { return this._propertyChangedDispatcher; } + set { this._propertyChangedDispatcher = value; } + } + + /// + /// We have to disable notifications when adding individual elements in the AddRange and RemoveRange implementations + /// + private bool isNotifying = true; + + public BindableCollection() : base() { } + public BindableCollection(IEnumerable collection) : base(collection) { } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + if (this.isNotifying) + this.PropertyChangedDispatcher(() => base.OnPropertyChanged(e)); + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (this.isNotifying) + this.PropertyChangedDispatcher(() => base.OnCollectionChanged(e)); + } + + /// + /// Add a range of items + /// + /// Items to add + public virtual void AddRange(IEnumerable items) + { + var previousNotificationSetting = this.isNotifying; + this.isNotifying = false; + var index = Count; + foreach (var item in items) + { + this.InsertItem(index, item); + index++; + } + this.isNotifying = previousNotificationSetting; + this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); + this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList())); + } + + /// + /// Remove a range of items + /// + /// Items to remove + public virtual void RemoveRange(IEnumerable items) + { + var previousNotificationSetting = this.isNotifying; + this.isNotifying = false; + foreach (var item in items) + { + var index = IndexOf(item); + if (index >= 0) + { + this.RemoveItem(index); + } + } + this.isNotifying = previousNotificationSetting; + this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); + this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items.ToList())); + } + + /// + /// Raise a change notification indicating that all bindings should be refreshed + /// + public void Refresh() + { + this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); + this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } +} diff --git a/Stylet/Bootstrapper.cs b/Stylet/Bootstrapper.cs new file mode 100644 index 0000000..ef21652 --- /dev/null +++ b/Stylet/Bootstrapper.cs @@ -0,0 +1,82 @@ +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + /// + /// Bootstrapper to be extended by any application which wants to use StyletIoC (the default) + /// + /// Type of the root ViewModel. This will be instantiated and displayed + public class Bootstrapper : BootstrapperBase where TRootViewModel : class + { + /// + /// IoC container. This is created after ConfigureIoC has been run. + /// + protected IContainer container; + + /// + /// Called from the constructor, this does everything necessary to start the application, including set up StyletIoC + /// + protected override void Start() + { + base.Start(); + + var builder = new StyletIoCBuilder(); + + this.DefaultConfigureIoC(builder); + this.ConfigureIoC(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 auto-bindings, so the user can replace them if they want + builder.BindWeak(typeof(IWindowManager)).To().InSingletonScope(); + builder.BindWeak(typeof(IEventAggregator)).To().InSingletonScope(); + builder.BindWeak(typeof(IViewManager)).To().InSingletonScope(); + + builder.Autobind(AssemblySource.Assemblies); + } + + /// + /// Override to add your own types to the IoC container. + /// + /// StyletIoC builder to use to configure the container + protected virtual void ConfigureIoC(IStyletIoCBuilder builder) { } + + /// + /// Override which uses StyletIoC as the implementation for IoC.Get + /// + protected override object GetInstance(Type service, string key = null) + { + return this.container.Get(service, key); + } + + /// + /// Override which uses StyletIoC as the implementation for IoC.GetAll + /// + protected override IEnumerable GetAllInstances(Type service) + { + return this.container.GetAll(service); + } + + /// + /// Override which uses StyletIoC as the implementation for IoC.BuildUp + /// + protected override void BuildUp(object instance) + { + this.container.BuildUp(instance); + } + } +} diff --git a/Stylet/BootstrapperBase.cs b/Stylet/BootstrapperBase.cs new file mode 100644 index 0000000..667c07f --- /dev/null +++ b/Stylet/BootstrapperBase.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; + +namespace Stylet +{ + /// + /// Bootstrapper to be extended by applications which don't want to use StyletIoC as the IoC container. + /// + /// Type of the root ViewModel. This will be instantiated and displayed + public abstract class BootstrapperBase where TRootViewModel : class + { + /// + /// Reference to the current application + /// + protected Application Application { get; private set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Start must be overridable. It doesn't depend on the type having been constructed")] + public BootstrapperBase() + { + this.Start(); + } + + /// + /// Called from the constructor, this does everything necessary to start the application + /// + protected virtual void Start() + { + this.Application = Application.Current; + + // Use the current SynchronizationContext for the Execute helper + Execute.SynchronizationContext = SynchronizationContext.Current; + + // Make life nice for the app - they can handle these by overriding Bootstrapper methods, rather than adding event handlers + this.Application.Startup += OnStartup; + this.Application.Exit += OnExit; + this.Application.DispatcherUnhandledException += OnUnhandledExecption; + + // The magic which actually displays + this.Application.Startup += (o, e) => + { + IoC.Get().ShowWindow(IoC.Get()); + }; + + // Add the current assembly to the assemblies list - this will be needed by the IViewManager + AssemblySource.Assemblies.Clear(); + AssemblySource.Assemblies.AddRange(this.SelectAssemblies()); + + this.ConfigureResources(); + + // Stitch the IoC shell to us + IoC.GetInstance = this.GetInstance; + IoC.GetAllInstances = this.GetAllInstances; + IoC.BuildUp = this.BuildUp; + } + + protected virtual void ConfigureResources() + { + var rc = new ResourceDictionary() { Source = new Uri("pack://application:,,,/Stylet;component/Xaml/StyletResourceDictionary.xaml", UriKind.Absolute) }; + Application.Resources.MergedDictionaries.Add(rc); + } + + /// + /// Override this to fetch an implementation of a service from your IoC container. Used by IoC.Get. + /// + /// Service type to fetch an implementation of + /// String key passed to IoC.Get + /// An instance implementing the service + protected abstract object GetInstance(Type service, string key = null); + + /// + /// Override this to fetch all implementations of a service from your IoC container. Used by IoC.GetAll. + /// + /// Service type to fetch all implementations for + /// All instances implementing the service + protected abstract IEnumerable GetAllInstances(Type service); + + /// + /// Override this to build up an instance using your IoC container. Used by IoC.BuildUp + /// + /// Instance to build up + protected abstract void BuildUp(object instance); + + /// + /// Initial contents of AssemblySource.Assemblies, defaults to the entry assembly + /// + /// + protected IEnumerable SelectAssemblies() + { + return new[] { Assembly.GetEntryAssembly() }; + } + + /// + /// Hook called on application startup + /// + protected virtual void OnStartup(object sender, StartupEventArgs e) { } + + /// + /// Hook called on application exit + /// + /// + /// + protected virtual void OnExit(object sender, EventArgs e) { } + + /// + /// Hook called on an unhandled exception + /// + protected virtual void OnUnhandledExecption(object sender, DispatcherUnhandledExceptionEventArgs e) { } + } +} diff --git a/Stylet/Conductor.cs b/Stylet/Conductor.cs new file mode 100644 index 0000000..71acfea --- /dev/null +++ b/Stylet/Conductor.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Conductor with a single active item, and no other items + /// + /// + public partial class Conductor : ConductorBaseWithActiveItem where T : class + { + /// + /// Activate the given item, discarding the previous ActiveItem + /// + /// + public override async void ActivateItem(T item) + { + if (item != null && item.Equals(this.ActiveItem)) + { + if (this.IsActive) + ScreenExtensions.TryActivate(item); + } + else if (await this.CanCloseItem(this.ActiveItem)) // This is null-safe + { + this.ChangeActiveItem(item, true); + } + } + + /// + /// Deactive the given item + /// + /// Item to deactivate + public override void DeactivateItem(T item) + { + if (item != null && item.Equals(this.ActiveItem)) + ScreenExtensions.TryDeactivate(this.ActiveItem); + } + + /// + /// Close the given item + /// + /// Item to close + public override async void CloseItem(T item) + { + if (item == null || !item.Equals(this.ActiveItem)) + return; + + if (await this.CanCloseItem(item)) + this.ChangeActiveItem(default(T), true); + + } + + /// + /// Determine if this conductor can close. Depends on whether the ActiveItem can close + /// + public override Task CanCloseAsync() + { + return this.CanCloseItem(this.ActiveItem); + } + } +} diff --git a/Stylet/ConductorAllActive.cs b/Stylet/ConductorAllActive.cs new file mode 100644 index 0000000..a72625b --- /dev/null +++ b/Stylet/ConductorAllActive.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + public partial class Conductor + { + public partial class Collections + { + /// + /// Conductor which has many items, all of which active at the same time + /// + public class AllActive : ConductorBase + { + private BindableCollection items = new BindableCollection(); + public IObservableCollection Items + { + get { return this.items; } + } + + public AllActive() + { + this.items.CollectionChanged += (o, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + this.ActivateAndSetParent(e.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + this.CloseAndCleanUp(e.OldItems); + break; + + case NotifyCollectionChangedAction.Replace: + this.ActivateAndSetParent(e.NewItems); + this.CloseAndCleanUp(e.OldItems); + break; + + case NotifyCollectionChangedAction.Reset: + this.ActivateAndSetParent(this.items); + break; + } + }; + } + + protected virtual void ActivateAndSetParent(IEnumerable items) + { + this.SetParent(items, true); + if (this.IsActive) + { + foreach (var item in items.OfType()) + { + item.Activate(); + } + } + } + + protected override void OnActivate() + { + foreach (var item in this.items.OfType()) + { + item.Activate(); + } + } + + protected override void OnDeactivate() + { + foreach (var item in this.items.OfType()) + { + item.Deactivate(); + } + } + + protected override void OnClose() + { + // We've already been deactivated by this point + foreach (var item in this.items) + this.CloseAndCleanUp(item); + + items.Clear(); + } + + /// + /// Determine if the conductor can close. Returns true if and when all items can close + /// + public override Task CanCloseAsync() + { + return this.CanAllItemsCloseAsync(this.items); + } + + /// + /// Activate the given item, and add it to the Items collection + /// + /// Item to activate + public override void ActivateItem(T item) + { + if (item == null) + return; + + item = this.EnsureItem(item); + + if (this.IsActive) + ScreenExtensions.TryActivate(item); + } + + /// + /// Deactive the given item + /// + /// Item to deactivate + public override void DeactivateItem(T item) + { + ScreenExtensions.TryDeactivate(item); + } + + /// + /// Close a particular item, removing it from the Items collection + /// + /// Item to close + public async override void CloseItem(T item) + { + if (item == null) + return; + + if (await this.CanCloseItem(item)) + { + this.CloseAndCleanUp(item); + this.items.Remove(item); + } + } + + /// + /// Returns all children of this parent + /// + public override IEnumerable GetChildren() + { + return this.items; + } + + protected override T EnsureItem(T newItem) + { + if (!this.items.Contains(newItem)) + this.items.Add(newItem); + + return base.EnsureItem(newItem); + } + } + } + } +} diff --git a/Stylet/ConductorBase.cs b/Stylet/ConductorBase.cs new file mode 100644 index 0000000..e7f5e2f --- /dev/null +++ b/Stylet/ConductorBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Base class for all conductors + /// + /// Type of item to be conducted + public abstract class ConductorBase : Screen, IConductor, IParent, IChildDelegate where T : class + { + public abstract IEnumerable GetChildren(); + + /// + /// Activate the given item + /// + /// Item to activate + public abstract void ActivateItem(T item); + + /// + /// Deactivate the given item + /// + /// Item to deactivate + public abstract void DeactivateItem(T item); + + /// + /// Deactivate the given item + /// + /// Item to deactivate + public abstract void CloseItem(T item); + + /// + /// Ensure an item is ready to be activated + /// + protected virtual T EnsureItem(T newItem) + { + var newItemAsChild = newItem as IChild; + if (newItemAsChild != null && newItemAsChild.Parent != this) + newItemAsChild.Parent = this; + + return newItem; + } + + /// + /// Utility method to determine if all of the give items can close + /// + protected virtual async Task CanAllItemsCloseAsync(IEnumerable toClose) + { + var results = await Task.WhenAll(toClose.Select(x => this.CanCloseItem(x))); + return results.All(x => x); + } + + /// + /// Determine if the given item can be closed + /// + /// + /// + protected virtual Task CanCloseItem(T item) + { + var itemAsGuardClose = item as IGuardClose; + if (itemAsGuardClose != null) + return itemAsGuardClose.CanCloseAsync(); + else + return Task.FromResult(true); + } + + void IChildDelegate.CloseItem(object item, bool? dialogResult) + { + T typedItem = item as T; + if (typedItem != null) + this.CloseItem(typedItem); + } + } +} diff --git a/Stylet/ConductorBaseWithActiveItem.cs b/Stylet/ConductorBaseWithActiveItem.cs new file mode 100644 index 0000000..c435421 --- /dev/null +++ b/Stylet/ConductorBaseWithActiveItem.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Base class for all conductors which had a single active item + /// + /// + public abstract class ConductorBaseWithActiveItem : ConductorBase, IHaveActiveItem where T : class + { + private T _activeItem; + + /// + /// Item which is currently active + /// + public T ActiveItem + { + get { return this._activeItem; } + set { this.ActivateItem(value); } + } + + /// + /// From IParent, fetch all items + /// + public override IEnumerable GetChildren() + { + return new[] { ActiveItem }; + } + + /// + /// Switch the active item to the given item + /// + protected virtual void ChangeActiveItem(T newItem, bool closePrevious) + { + ScreenExtensions.TryDeactivate(this.ActiveItem); + if (closePrevious) + this.CloseAndCleanUp(this.ActiveItem); + + newItem = this.EnsureItem(newItem); + + if (this.IsActive) + ScreenExtensions.TryActivate(newItem); + + this._activeItem = newItem; + this.NotifyOfPropertyChange(() => this.ActiveItem); + } + + /// + /// When we're activated, also activate the ActiveItem + /// + protected override void OnActivate() + { + ScreenExtensions.TryActivate(this.ActiveItem); + } + + /// + /// When we're deactivated, also deactivate the ActiveItem + /// + protected override void OnDeactivate() + { + ScreenExtensions.TryDeactivate(this.ActiveItem); + } + + /// + /// When we're closed, also close the ActiveItem + /// + protected override void OnClose() + { + this.CloseAndCleanUp(this.ActiveItem); + } + } + +} diff --git a/Stylet/ConductorNavigating.cs b/Stylet/ConductorNavigating.cs new file mode 100644 index 0000000..37db9aa --- /dev/null +++ b/Stylet/ConductorNavigating.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + public partial class Conductor + { + public partial class Collections + { + /// + /// Stack-based navigation. A Conductor which has one active item, and a stack of previous items + /// + public class Navigation : ConductorBaseWithActiveItem + { + // We need to remove arbitrary items, so no Stack here! + private List history = new List(); + + /// + /// Activate the given item. This deactivates the previous item, and pushes it onto the history stack + /// + /// Item to activate + public override void ActivateItem(T item) + { + if (item != null && item.Equals(this.ActiveItem)) + { + if (this.IsActive) + ScreenExtensions.TryActivate(this.ActiveItem); + } + else + { + if (this.ActiveItem != null) + this.history.Add(this.ActiveItem); + this.ChangeActiveItem(item, false); + } + } + + /// + /// Deactivate the given item + /// + /// Item to deactivate + public override void DeactivateItem(T item) + { + ScreenExtensions.TryDeactivate(item); + } + + /// + /// Close the active item, and re-activate the top item in the history stack + /// + public void GoBack() + { + this.CloseItem(this.ActiveItem); + } + + /// + /// Close and remove all items in the history stack, leaving the ActiveItem + /// + public void Clear() + { + foreach (var item in this.history) + this.CloseAndCleanUp(item); + this.history.Clear(); + } + + /// + /// Close the given item. If it was the ActiveItem, activate the top item in the history stack + /// + /// + public override async void CloseItem(T item) + { + if (item == null || !await this.CanCloseItem(item)) + return; + + if (item.Equals(this.ActiveItem)) + { + var newItem = default(T); + if (this.history.Count > 0) + { + newItem = this.history.Last(); + this.history.RemoveAt(this.history.Count-1); + } + this.ChangeActiveItem(newItem, true); + } + else if (this.history.Contains(item)) + { + this.CloseAndCleanUp(item); + this.history.Remove(item); + } + } + + /// + /// Returns true if and when all items (ActiveItem + everything in the history stack) can close + /// + /// + public override Task CanCloseAsync() + { + return this.CanAllItemsCloseAsync(this.history.Concat(new[] { this.ActiveItem })); + } + + protected override void OnClose() + { + // We've already been deactivated by this point + foreach (var item in this.history) + this.CloseAndCleanUp(item); + this.history.Clear(); + + this.CloseAndCleanUp(this.ActiveItem); + } + } + } + } +} diff --git a/Stylet/ConductorOneActive.cs b/Stylet/ConductorOneActive.cs new file mode 100644 index 0000000..37981a7 --- /dev/null +++ b/Stylet/ConductorOneActive.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + public partial class Conductor + { + public partial class Collections + { + /// + /// Conductor with many items, only one of which is active + /// + public class OneActive : ConductorBaseWithActiveItem + { + private BindableCollection items = new BindableCollection(); + public IObservableCollection Items + { + get { return this.items; } + } + + public OneActive() + { + this.items.CollectionChanged += (o, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + this.SetParent(e.NewItems, true); + break; + + case NotifyCollectionChangedAction.Remove: + this.CloseAndCleanUp(e.OldItems); + this.ActiveItemMayHaveBeenRemovedFromItems(); + break; + + case NotifyCollectionChangedAction.Replace: + this.SetParent(e.NewItems, true); + this.CloseAndCleanUp(e.OldItems); + this.ActiveItemMayHaveBeenRemovedFromItems(); + break; + + case NotifyCollectionChangedAction.Reset: + this.SetParent(this.items, true); + this.ActiveItemMayHaveBeenRemovedFromItems(); + break; + } + }; + } + + protected virtual void ActiveItemMayHaveBeenRemovedFromItems() + { + if (this.items.Contains(this.ActiveItem)) + return; + + this.ChangeActiveItem(this.items.FirstOrDefault(), true); + } + + public override IEnumerable GetChildren() + { + return this.items; + } + + /// + /// Activate the given item and set it as the ActiveItem, deactivating the previous ActiveItem + /// + /// Item to deactivate + public override void ActivateItem(T item) + { + if (item != null && item.Equals(this.ActiveItem)) + { + if (this.IsActive) + ScreenExtensions.TryActivate(this.ActiveItem); + } + else + { + this.ChangeActiveItem(item, false); + } + } + + /// + /// Deactive the given item, and choose another item to set as the ActiveItem + /// + /// Item to deactivate + public override void DeactivateItem(T item) + { + if (item == null) + return; + + if (item.Equals(this.ActiveItem)) + { + var nextItem = this.DetermineNextItemToActivate(this.items, this.items.IndexOf(item)); + this.ChangeActiveItem(null, false); + } + else + { + ScreenExtensions.TryDeactivate(item); + } + } + + public override async void CloseItem(T item) + { + if (item == null || !await this.CanCloseItem(item)) + return; + + if (item.Equals(this.ActiveItem)) + { + var nextItem = this.DetermineNextItemToActivate(this.items, this.items.IndexOf(item)); + this.ChangeActiveItem(nextItem, true); + } + else + { + this.CloseAndCleanUp(item); + } + + this.items.Remove(item); + } + + protected virtual T DetermineNextItemToActivate(IList list, int indexOfItemBeingRemoved) + { + // indexOfItemBeingRemoved *can* be -1 - if the item being removed doesn't exist in the list + if (list.Count > 1) + { + if (indexOfItemBeingRemoved < 0) + return list[0]; + else if (indexOfItemBeingRemoved == 0) + return list[1]; + else + return list[indexOfItemBeingRemoved - 1]; + } + else + { + return default(T); + } + } + + /// + /// Returns true if and when all children can close + /// + /// + public override Task CanCloseAsync() + { + return this.CanAllItemsCloseAsync(this.items); + } + + protected override void OnClose() + { + // We've already been deactivated by this point + foreach (var item in this.items) + this.CloseAndCleanUp(item); + this.items.Clear(); + } + + protected override T EnsureItem(T newItem) + { + if (newItem == null) + { + newItem = this.DetermineNextItemToActivate(this.items, this.ActiveItem == null ? 0 : this.items.IndexOf(this.ActiveItem)); + } + else + { + if (!this.items.Contains(newItem)) + this.items.Add(newItem); + } + + return base.EnsureItem(newItem); + } + } + } + } +} diff --git a/Stylet/EventAggregator.cs b/Stylet/EventAggregator.cs new file mode 100644 index 0000000..850203c --- /dev/null +++ b/Stylet/EventAggregator.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Marker for types which we might be interested in + /// + public interface IHandle + { + } + + /// + /// Implement this to handle a particular message type + /// + /// Message type to handle. Can be a base class of the messsage type(s) to handle + public interface IHandle : IHandle + { + void Handle(TMessageType message); + } + + /// + /// Centralised, weakly-binding publish/subscribe event manager + /// + public interface IEventAggregator + { + /// + /// Register an instance as wanting to receive events. Implement IHandle{T} for each event type you want to receive. + /// + /// Instance that will be registered with the EventAggregator + void Subscribe(IHandle handler); + + /// + /// Unregister as wanting to receive events. The instance will no longer receive events after this is called. + /// + /// Instance to unregister + void Unsubscribe(IHandle handler); + + /// + /// Publish an event to all subscribers, using the specified dispatcher + /// + /// Event to publish + /// Dispatcher to use to call each subscriber's handle method(s) + void PublishWithDispatcher(object message, Action dispatcher); + + /// + /// Publish an event to all subscribers, calling the handle methods on the UI thread + /// + /// Event to publish + void PublishOnUIThread(object message); + + /// + /// Publish an event to all subscribers, calling the handle methods synchronously on the current thread + /// + /// Event to publish + void Publish(object message); + } + + public class EventAggregator : IEventAggregator + { + private readonly List handlers = new List(); + private readonly object handlersLock = new object(); + + public void Subscribe(IHandle handler) + { + lock (this.handlersLock) + { + // Is it already subscribed? + if (this.handlers.Any(x => x.IsHandlerForInstance(handler))) + return; + + this.handlers.Add(new Handler(handler)); + } + } + + public void Unsubscribe(IHandle handler) + { + lock (this.handlersLock) + { + var existingHandler = this.handlers.FirstOrDefault(x => x.IsHandlerForInstance(handler)); + if (existingHandler != null) + this.handlers.Remove(existingHandler); + } + } + + public void PublishWithDispatcher(object message, Action dispatcher) + { + lock (this.handlersLock) + { + var messageType = message.GetType(); + var deadHandlers = this.handlers.Where(x => !x.Handle(messageType, message, dispatcher)).ToArray(); + foreach (var deadHandler in deadHandlers) + { + this.handlers.Remove(deadHandler); + } + } + } + + public void PublishOnUIThread(object message) + { + this.PublishWithDispatcher(message, Execute.OnUIThread); + } + + public void Publish(object message) + { + this.PublishWithDispatcher(message, x => x()); + } + + private class Handler + { + private readonly WeakReference target; + private readonly List invokers = new List(); + + public Handler(object handler) + { + var handlerType = handler.GetType(); + this.target = new WeakReference(handler); + + foreach (var implementation in handler.GetType().GetInterfaces().Where(x => x.IsGenericType && typeof(IHandle).IsAssignableFrom(x))) + { + var messageType = implementation.GetGenericArguments()[0]; + this.invokers.Add(new HandlerInvoker(handlerType, messageType, implementation.GetMethod("Handle"))); + } + } + + public bool IsHandlerForInstance(object subscriber) + { + return this.target.Target == subscriber; + } + + public bool Handle(Type messageType, object message, Action dispatcher) + { + var target = this.target.Target; + if (target == null) + return false; + + foreach (var invoker in this.invokers) + { + invoker.Invoke(target, messageType, message, dispatcher); + } + + return true; + } + } + + private class HandlerInvoker + { + private readonly Type messageType; + private readonly Action invoker; + + public HandlerInvoker(Type targetType, Type messageType, MethodInfo invocationMethod) + { + this.messageType = messageType; + var targetParam = Expression.Parameter(typeof(object), "target"); + var messageParam = Expression.Parameter(typeof(object), "message"); + var castTarget = Expression.Convert(targetParam, targetType); + var castMessage = Expression.Convert(messageParam, messageType); + var callExpression = Expression.Call(castTarget, invocationMethod, castMessage); + this.invoker = Expression.Lambda>(callExpression, targetParam, messageParam).Compile(); + } + + public void Invoke(object target, Type messageType, object message, Action dispatcher) + { + if (this.messageType.IsAssignableFrom(messageType)) + dispatcher(() => this.invoker(target, message)); + } + } + } +} diff --git a/Stylet/Execute.cs b/Stylet/Execute.cs new file mode 100644 index 0000000..acbc177 --- /dev/null +++ b/Stylet/Execute.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + public static class Execute + { + public static SynchronizationContext SynchronizationContext; + private static bool? inDesignMode; + + public static Action DefaultPropertyChangedDispatcher = a => a(); + + public static void OnUIThread(Action action) + { + // If we're already on the given SynchronizationContext, or it hasn't been set, run synchronously + if (SynchronizationContext != null && SynchronizationContext != SynchronizationContext.Current) + SynchronizationContext.Post(_ => action(), null); + else + action(); + } + + public static Task OnUIThreadAsync(Action action) + { + // If we're already on the given SynchronizationContext, or it hasn't been set, run synchronously + if (SynchronizationContext != null && SynchronizationContext != SynchronizationContext.Current) + { + var tcs = new TaskCompletionSource(); + SynchronizationContext.Post(_ => + { + try + { + action(); + tcs.SetResult(null); + } + catch (Exception e) + { + tcs.SetException(e); + } + }, null); + return tcs.Task; + } + else + { + action(); + return Task.FromResult(false); + } + } + + public static bool InDesignMode + { + get + { + if (inDesignMode == null) + { + var prop = DesignerProperties.IsInDesignModeProperty; + inDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(prop, typeof(FrameworkElement)).Metadata.DefaultValue; + + if (inDesignMode.GetValueOrDefault(false) && Process.GetCurrentProcess().ProcessName.StartsWith("devenv", StringComparison.Ordinal)) + inDesignMode = true; + } + + return inDesignMode.GetValueOrDefault(false); + } + } + } +} diff --git a/Stylet/ExpressionExtensions.cs b/Stylet/ExpressionExtensions.cs new file mode 100644 index 0000000..f497622 --- /dev/null +++ b/Stylet/ExpressionExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Useful extension methods on Expressions + /// + public static class ExpressionExtensions + { + /// + /// Given a MemberExpression (or MemberExpression wrapped in a UnaryExpression), get the name of the property + /// + /// Name of the property referenced by the expression + public static string NameForProperty(this Expression propertyExpression) + { + Expression body; + if (propertyExpression.Body is UnaryExpression) + body = ((UnaryExpression)propertyExpression.Body).Operand; + else + body = propertyExpression.Body; + + var member = body as MemberExpression; + if (member == null) + throw new ArgumentException("Property must be a MemberExpression"); + + return member.Member.Name; + } + } +} diff --git a/Stylet/GlobalSuppressions.cs b/Stylet/GlobalSuppressions.cs new file mode 100644 index 0000000..a6f8994 --- /dev/null +++ b/Stylet/GlobalSuppressions.cs @@ -0,0 +1,5 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// +// You do not need to add suppressions to this file manually. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Scope = "type", Target = "StyletIoC.StyletIoCException", Justification = "Exceptions aren't used across AppDomains")] \ No newline at end of file diff --git a/Stylet/IConductor.cs b/Stylet/IConductor.cs new file mode 100644 index 0000000..aadc73a --- /dev/null +++ b/Stylet/IConductor.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Generalised parent, with many children + /// + public interface IParent + { + /// + /// Fetch all children of this parent + /// + /// + IEnumerable GetChildren(); + } + + /// + /// Thing which has a single active item + /// + /// Type of the active item + public interface IHaveActiveItem + { + T ActiveItem { get; set; } + } + + /// + /// Thing which has one or more children, and from which a child can request that it be closed + /// + public interface IChildDelegate + { + void CloseItem(object item, bool? dialogResult = null); + } + + /// + /// Thing which owns one or more children, and can manage their lifecycles accordingly + /// + public interface IConductor + { + /// + /// Activate the given item + /// + /// Item to activate + void ActivateItem(T item); + + /// + /// Deactivate the given item + /// + /// Item to deactivate + void DeactivateItem(T item); + + /// + /// Close the given item + /// + /// Item to close + void CloseItem(T item); + } +} diff --git a/Stylet/INotifyPropertyChangedDispatcher.cs b/Stylet/INotifyPropertyChangedDispatcher.cs new file mode 100644 index 0000000..20ed073 --- /dev/null +++ b/Stylet/INotifyPropertyChangedDispatcher.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Knows how to dispatch its PropertyChanged events using a given dispatcher + /// + public interface INotifyPropertyChangedDispatcher + { + /// + /// The dispatcher to use. Called with an action, which should itself be called in the appropriate context + /// + Action PropertyChangedDispatcher { get; set; } + } +} diff --git a/Stylet/IScreen.cs b/Stylet/IScreen.cs new file mode 100644 index 0000000..10b5611 --- /dev/null +++ b/Stylet/IScreen.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + /// + /// Is aware of the fact that it has a view + /// + public interface IViewAware + { + /// + /// The view associated with this ViewModel + /// + UIElement View { get; } + + /// + /// Called when the view should be attached. Should set View property. + /// + /// Separate from the View property so it can be explicitely implemented + /// View to attach + void AttachView(UIElement view); + } + + /// + /// Can be activated, and raises an event when it is actually activated + /// + public interface IActivate + { + /// + /// Activate the object. May not actually cause activation (e.g. if it's already active) + /// + void Activate(); + + /// + /// Raised when the object is actually activated + /// + event EventHandler Activated; + } + + /// + /// Can be deactivated, and raises an event when it is actually deactivated + /// + public interface IDeactivate + { + /// + /// Deactivate the object. May not actually cause deactivation (e.g. if it's already deactive) + /// + void Deactivate(); + + /// + /// Raised when the object is actually deactivated + /// + event EventHandler Deactivated; + } + + /// + /// Can be closed, and raises an event when it is actually closed + /// + public interface IClose + { + /// + /// Close the object. May not actually cause closure (e.g. if it's already closed) + /// + void Close(); + + /// + /// Raised when the object is actually closed + /// + event EventHandler Closed; + } + + /// + /// Has a display name. In reality, this is bound to things like Window titles and TabControl tabs + /// + public interface IHaveDisplayName + { + /// + /// Name which should be displayed + /// + string DisplayName { get; set; } + } + + /// + /// Acts as a child. Knows about its parent + /// + public interface IChild + { + /// + /// Parent object to this child + /// + object Parent { get; set; } + } + + /// + /// Has an opinion on whether it should be closed + /// + /// If implemented, CanCloseAsync should be called prior to closing the object + public interface IGuardClose + { + /// + /// Returns whether or not the object can close, potentially asynchronously + /// + Task CanCloseAsync(); + } + + /// + /// Generalised 'screen' composing all the behaviours expected of a screen + /// + public interface IScreen : IViewAware, IHaveDisplayName, IActivate, IDeactivate, IChild, IClose, IGuardClose + { + } + + + public class ActivationEventArgs : EventArgs + { + } + + public class DeactivationEventArgs : EventArgs + { + } + + public class CloseEventArgs : EventArgs + { + } +} diff --git a/Stylet/IoC.cs b/Stylet/IoC.cs new file mode 100644 index 0000000..716a45d --- /dev/null +++ b/Stylet/IoC.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// A lightweight wrapper around the IoC container of your choice. Configured in the bootstrapper + /// + public static class IoC + { + /// + /// Assign this to a func which can get a single instance of a service, with a given key + /// + public static Func GetInstance = (service, key) => { throw new InvalidOperationException("IoC is not initialized"); }; + + /// + /// Assign this to a func which can get an IEnumerable of all instances of a service + /// + public static Func> GetAllInstances = service => { throw new InvalidOperationException("IoC is not initialized"); }; + + /// + /// Assign this to a fun which can build up a given object + /// + public static Action BuildUp = instance => { throw new InvalidOperationException("IoC is not initialized"); }; + + /// + /// Wraps GetInstance, adding typing + /// + public static T Get(string key = null) + { + return (T)GetInstance(typeof(T), key); + } + + /// + /// Wraps GetAllInstances, adding typing + /// + public static IEnumerable GetAll() + { + return GetAllInstances(typeof(T)).Cast(); + } + } +} diff --git a/Stylet/LabelledValue.cs b/Stylet/LabelledValue.cs new file mode 100644 index 0000000..1d91ef8 --- /dev/null +++ b/Stylet/LabelledValue.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Key-value pair useful for attaching labels to objects and displaying them in the view + /// + /// Type of the value + public class LabelledValue : PropertyChangedBase, IEquatable> + { + private string _label; + /// + /// Label associated with this item. This is displayed in your View + /// + public string Label + { + get { return this._label; } + set { SetAndNotify(ref this._label, value); } + } + + private T _value; + /// + /// Value associated with this item. This is used by your ViewModel + /// + public T Value + { + get { return this._value; } + set { SetAndNotify(ref this._value, value); } + } + + public LabelledValue(string label, T value) + { + this._label = label; + this._value = value; + } + + public bool Equals(LabelledValue other) + { + return other == null ? false : this.Label == other.Label && EqualityComparer.Default.Equals(this.Value, other.Value); + } + + public override bool Equals(object obj) + { + return this.Equals(obj as LabelledValue); + } + + public override int GetHashCode() + { + return new { this.Label, this.Value }.GetHashCode(); + } + + public override string ToString() + { + return this.Label; + } + } + + /// + /// Convenience class for constructing LabellelValue{T}'s + /// + public static class LabelledValue + { + /// + /// Construct a new LabelledValue{T}, using method type inference + /// + public static LabelledValue Create(string label, T value) + { + return new LabelledValue(label, value); + } + } +} diff --git a/Stylet/Properties/AssemblyInfo.cs b/Stylet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1890254 --- /dev/null +++ b/Stylet/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows.Markup; + +// 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")] +[assembly: AssemblyDescription("A very lightweight but powerful ViewModel-First MVVM framework for WPF, inspired by Caliburn.Micro. Comes with its own IoC container.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Antony Male")] +[assembly: AssemblyProduct("Stylet")] +[assembly: AssemblyCopyright("Copyright © Antony Male 2014")] +[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)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a557a739-6b61-44d2-a431-889bc11aac9e")] + +[assembly: XmlnsDefinition("http://github.com/canton7/Stylet", "Stylet")] + +// 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/Stylet/PropertyChangedBase.cs b/Stylet/PropertyChangedBase.cs new file mode 100644 index 0000000..637ad6a --- /dev/null +++ b/Stylet/PropertyChangedBase.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// Base class for things which can raise PropertyChanged events + /// + public abstract class PropertyChangedBase : INotifyPropertyChanged, INotifyPropertyChangedDispatcher + { + private Action _propertyChangedDispatcher = Execute.DefaultPropertyChangedDispatcher; + /// + /// Dispatcher to use to dispatch PropertyChanged events. Defaults to Execute.DefaultPropertyChangedDispatcher + /// + public virtual Action PropertyChangedDispatcher + { + get { return this._propertyChangedDispatcher; } + set { this._propertyChangedDispatcher = value; } + } + + /// + /// Occurs when a property value changes + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Refresh all properties + /// + public void Refresh() + { + this.NotifyOfPropertyChange(String.Empty); + } + + /// + /// Raise a PropertyChanged notification from the property in the given expression, e.g. NotifyOfPropertyChange(() => this.Property) + /// Expression describing the property to raise a PropertyChanged notification for + protected virtual void NotifyOfPropertyChange(Expression> property) + { + this.OnPropertyChanged(property.NameForProperty()); + } + + /// + /// Raise a PropertyChanged notification from the property with the given name + /// + /// Name of the property to raise a PropertyChanged notification for. Defaults to the calling property + protected virtual void NotifyOfPropertyChange([CallerMemberName] string propertyName = "") + { + this.OnPropertyChanged(propertyName); + } + + /// + /// Fires the PropertyChanged notification. + /// + /// Specially named so that Fody.PropertyChanged calls it + /// Name of the property to raise the notification for + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void OnPropertyChanged(string propertyName) + { + var handler = this.PropertyChanged; + if (handler != null) + { + this.PropertyChangedDispatcher(() => handler(this, new PropertyChangedEventArgs(propertyName))); + } + } + + /// + /// Takes, by reference, a field, and its new value. If field != value, will set field = value and raise a PropertyChanged notification + /// + /// Field to assign + /// Value to assign to the field, if it differs + /// Name of the property to notify for. Defaults to the calling property + protected virtual void SetAndNotify(ref T field, T value, [CallerMemberName] string propertyName = "") + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + this.NotifyOfPropertyChange(propertyName); + } + } + } +} diff --git a/Stylet/PropertyChangedExtensions.cs b/Stylet/PropertyChangedExtensions.cs new file mode 100644 index 0000000..9f7a0c2 --- /dev/null +++ b/Stylet/PropertyChangedExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + /// + /// A binding to a PropertyChanged event, which can be used to unbind the binding + /// + public interface IPropertyChangedBinding + { + void Unbind(); + } + + public static class PropertyChangedExtensions + { + internal class StrongPropertyChangedBinding : IPropertyChangedBinding + { + private WeakReference inpc; + private PropertyChangedEventHandler handler; + + public StrongPropertyChangedBinding(INotifyPropertyChanged inpc, PropertyChangedEventHandler handler) + { + this.inpc = new WeakReference(inpc); + this.handler = handler; + } + + public void Unbind() + { + INotifyPropertyChanged inpc; + if (this.inpc.TryGetTarget(out inpc)) + { + inpc.PropertyChanged -= handler; + } + } + } + + /// + /// Strongly bind to PropertyChanged events for a particular property on a particular object + /// + /// someObject.Bind(x => x.PropertyNameToBindTo, newValue => /* do something with the new value */) + /// Object raising the PropertyChanged event you're interested in + /// MemberExpression selecting the property to observe for changes (e.g x => x.PropertyName) + /// Handler called whenever that property changed + /// Something which can be used to undo the binding. You can discard it if you want + public static IPropertyChangedBinding Bind(this TBindTo target, Expression> targetSelector, Action handler) where TBindTo : class, INotifyPropertyChanged + { + var propertyName = targetSelector.NameForProperty(); + var propertyAccess = targetSelector.Compile(); + // Make sure we don't capture target strongly, otherwise we'll retain it when we shouldn't + // If it does get released, we're released from the delegate list + var weakTarget = new WeakReference(target); + + PropertyChangedEventHandler ourHandler = (o, e) => + { + if (e.PropertyName == propertyName || e.PropertyName == String.Empty) + { + TBindTo strongTarget; + if (weakTarget.TryGetTarget(out strongTarget)) + handler(propertyAccess(strongTarget)); + } + }; + + target.PropertyChanged += ourHandler; + + var listener = new StrongPropertyChangedBinding(target, ourHandler); + + return listener; + } + } +} diff --git a/Stylet/Screen.cs b/Stylet/Screen.cs new file mode 100644 index 0000000..52febf7 --- /dev/null +++ b/Stylet/Screen.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls.Primitives; + +namespace Stylet +{ + /// + /// Implementation of IScreen. Useful as a base class for your ViewModels + /// + public class Screen : PropertyChangedBase, IScreen + { + #region WeakEventManager + + private Lazy lazyWeakEventManager = new Lazy(() => new WeakEventManager(), true); + /// + /// WeakEventManager owned by this screen (lazy) + /// + protected IWeakEventManager weakEventManager { get { return this.lazyWeakEventManager.Value; } } + + /// + /// Proxy around this.weakEventManager.BindWeak. Binds to an INotifyPropertyChanged source, in a way which doesn't cause us to be retained + /// + /// this.BindWeak(objectToBindTo, x => x.PropertyToBindTo, newValue => handlerForNewValue) + /// Object to observe for PropertyChanged events + /// Expression for selecting the property to observe, e.g. x => x.PropertyName + /// Handler to be called when that property changes + /// A resource which can be used to undo the binding + protected IPropertyChangedBinding BindWeak(TSource source, Expression> selector, Action handler) + where TSource : class, INotifyPropertyChanged + { + return this.weakEventManager.BindWeak(source, selector, handler); + } + + #endregion + + #region IHaveDisplayName + + private string _displayName; + public string DisplayName + { + get { return this._displayName; } + set { SetAndNotify(ref this._displayName, value); } + } + + #endregion + + #region IActivate + + public event EventHandler Activated; + + private bool hasBeenActivatedEver; + + private bool _isActive; + /// + /// True if this Screen is currently active + /// + public bool IsActive + { + get { return this._isActive; } + set { SetAndNotify(ref this._isActive, value); } + } + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "As this is a framework type, don't want to make it too easy for users to call this method")] + void IActivate.Activate() + { + if (this.IsActive) + return; + + this.IsActive = true; + + if (!this.hasBeenActivatedEver) + this.OnInitialActivate(); + this.hasBeenActivatedEver = true; + + this.OnActivate(); + + var handler = this.Activated; + if (handler != null) + handler(this, new ActivationEventArgs()); + } + + /// + /// Called the very first time this Screen is activated, and never again + /// + protected virtual void OnInitialActivate() { } + + /// + /// Called every time this screen is activated + /// + protected virtual void OnActivate() { } + + #endregion + + #region IDeactivate + + public event EventHandler Deactivated; + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "Stylet.Screen.#Stylet.IDeactivate.Deactivate()", Justification = "As this is a framework type, don't want to make it too easy for users to call this method")] + void IDeactivate.Deactivate() + { + if (!this.IsActive) + return; + + this.IsActive = false; + + this.OnDeactivate(); + + var handler = this.Deactivated; + if (handler != null) + handler(this, new DeactivationEventArgs()); + } + + /// + /// Called every time this screen is deactivated + /// + protected virtual void OnDeactivate() { } + + #endregion + + #region IClose + + public event EventHandler Closed; + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "As this is a framework type, don't want to make it too easy for users to call this method")] + void IClose.Close() + { + // This will early-exit if it's already deactive + ((IDeactivate)this).Deactivate(); + + this.View = null; + + this.OnClose(); + + var handler = this.Closed; + if (handler != null) + handler(this, new CloseEventArgs()); + } + + /// + /// Called when this screen is closed + /// + protected virtual void OnClose() { } + + #endregion + + #region IViewAware + + public UIElement View { get; private set; } + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "As this is a framework type, don't want to make it too easy for users to call this method")] + void IViewAware.AttachView(UIElement view) + { + if (this.View != null) + throw new Exception(String.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, this.GetType().Name)); + + this.View = view; + + var viewAsFrameworkElement = view as FrameworkElement; + if (viewAsFrameworkElement != null) + { + if (viewAsFrameworkElement.IsLoaded) + this.OnViewLoaded(); + else + viewAsFrameworkElement.Loaded += (o, e) => this.OnViewLoaded(); + } + } + + /// + /// Called when the view attaches to the Screen loads + /// + protected virtual void OnViewLoaded() { } + + #endregion + + #region IChild + + private object _parent; + /// + /// Parent conductor of this screen. Used to TryClose to request a closure + /// + public object Parent + { + get { return this._parent; } + set { SetAndNotify(ref this._parent, value); } + } + + #endregion + + #region IGuardClose + + /// + /// Called when a conductor wants to know whether this screen can close. + /// + /// A task returning true (can close) or false (can't close) + public virtual Task CanCloseAsync() + { + return Task.FromResult(true); + } + + #endregion + + /// + /// Request that the conductor responsible for this screen close it + /// + /// + public virtual void TryClose(bool? dialogResult = null) + { + var conductor = this.Parent as IChildDelegate; + if (conductor != null) + conductor.CloseItem(this, dialogResult); + else + throw new InvalidOperationException(String.Format("Unable to close ViewModel {0} as it must have a conductor as a parent (note that windows and dialogs automatically have such a parent)", this.GetType().Name)); + } + } +} diff --git a/Stylet/ScreenExtensions.cs b/Stylet/ScreenExtensions.cs new file mode 100644 index 0000000..8deb627 --- /dev/null +++ b/Stylet/ScreenExtensions.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + /// + /// Handy extensions for working with screens + /// + public static class ScreenExtensions + { + /// + /// Attempt to activate the screen, if it implements IActivate + /// + /// Screen to activate + public static void TryActivate(object screen) + { + var screenAsActivate = screen as IActivate; + if (screenAsActivate != null) + screenAsActivate.Activate(); + } + + /// + /// Attempt to deactivate the screen, if it implements IDeactivate + /// + /// Screen to deactivate + public static void TryDeactivate(object screen) + { + var screenAsDeactivate = screen as IDeactivate; + if (screenAsDeactivate != null) + screenAsDeactivate.Deactivate(); + } + + /// + /// Try to close the screen, if it implements IClose + /// + /// Screen to close + public static void TryClose(object screen) + { + var screenAsClose = screen as IClose; + if (screenAsClose != null) + screenAsClose.Close(); + } + + /// + /// Activate the child whenever the parent is activated + /// + /// child.ActivateWith(this) + public static void ActivateWith(this IActivate child, IActivate parent) + { + WeakEventManager.AddHandler(parent, "Activated", (o, e) => child.Activate()); + } + + /// + /// Deactivate the child whenever the parent is deactivated + /// + /// child.DeactivateWith(this) + public static void DeactivateWith(this IDeactivate child, IDeactivate parent) + { + WeakEventManager.AddHandler(parent, "Deactivated", (o, e) => child.Deactivate()); + } + + /// + /// Close the child whenever the parent is closed + /// + /// child.CloseWith(this) + public static void CloseWith(this IClose child, IClose parent) + { + WeakEventManager.AddHandler(parent, "Closed", (o, e) => child.Close()); + } + + /// + /// Activate, Deactivate, or Close the child whenever the parent is Activated, Deactivated, or Closed + /// + /// child.ConductWith(this) + public static void ConductWith(this TChild child, TParent parent) + where TChild : IActivate, IDeactivate, IClose + where TParent : IActivate, IDeactivate, IClose + { + child.ActivateWith(parent); + child.DeactivateWith(parent); + child.CloseWith(parent); + } + } +} diff --git a/Stylet/Stylet.csproj b/Stylet/Stylet.csproj new file mode 100644 index 0000000..a5dd02c --- /dev/null +++ b/Stylet/Stylet.csproj @@ -0,0 +1,102 @@ + + + + + Debug + AnyCPU + {2435BD00-AC12-48B0-AD36-9BAB2FDEC3F5} + Library + Properties + Stylet + Stylet + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + + + \ No newline at end of file diff --git a/Stylet/StyletConductorExtensions.cs b/Stylet/StyletConductorExtensions.cs new file mode 100644 index 0000000..3d3e788 --- /dev/null +++ b/Stylet/StyletConductorExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + // Don't name ConductorExtensions, otherwise it's too obvious when someone types 'Conductor' + public static class StyletConductorExtensions + { + public static void SetParent(this IConductor parent, IEnumerable items, bool setOrClear) + { + foreach (var child in items.OfType()) + { + if (setOrClear) + child.Parent = parent; + else if (child.Parent == parent) + child.Parent = null; + } + } + + /// + /// Close an item, and clean it up a bit + /// + public static void CloseAndCleanUp(this IConductor parent, T item) + { + ScreenExtensions.TryClose(item); + + var itemAsChild = item as IChild; + if (itemAsChild != null && itemAsChild.Parent == parent) + itemAsChild.Parent = null; + } + + public static void CloseAndCleanUp(this IConductor parent, IEnumerable items) + { + foreach (var item in items.OfType()) + { + item.Close(); + } + + parent.SetParent(items, false); + } + } +} diff --git a/Stylet/StyletIoC/BuilderUpper.cs b/Stylet/StyletIoC/BuilderUpper.cs new file mode 100644 index 0000000..9d5b1a6 --- /dev/null +++ b/Stylet/StyletIoC/BuilderUpper.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIoC +{ + internal class BuilderUpper + { + private Type type; + private StyletIoCContainer container; + private Action implementor; + + public BuilderUpper(Type type, StyletIoCContainer container) + { + this.type = type; + this.container = container; + } + + public Expression GetExpression(Expression inputParameterExpression) + { + var expressions = this.type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Select(x => this.ExpressionForMember(inputParameterExpression, x, x.FieldType)) + .Concat(this.type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Select(x => this.ExpressionForMember(inputParameterExpression, x, x.PropertyType))) + .Where(x => x != null); + + // Sadly, we can't cache this expression (I think), as it relies on the inputParameterExpression + // which is likely to change between calls + // This isn't so bad, so we'll (probably) only need to call this at most twice - once for building up the type on creation, + // and once for creating the implemtor (which is used in BuildUp()) + if (!expressions.Any()) + return Expression.Empty(); + return Expression.Block(expressions); + } + + private Expression ExpressionForMember(Expression objExpression, MemberInfo member, Type memberType) + { + var attribute = member.GetCustomAttribute(true); + if (attribute == null) + return null; + + var memberAccess = Expression.MakeMemberAccess(objExpression, member); + var memberValue = this.container.GetExpression(new TypeKey(memberType, attribute.Key), true); + return Expression.Assign(memberAccess, memberValue); + } + + public Action GetImplementor() + { + if (this.implementor != null) + return this.implementor; + + var parameterExpression = Expression.Parameter(typeof(object), "inputParameter"); + var typedParameterExpression = Expression.Convert(parameterExpression, this.type); + var expression = this.GetExpression(typedParameterExpression); + this.implementor = Expression.Lambda>(this.GetExpression(typedParameterExpression), parameterExpression).Compile(); + + return this.implementor; + } + } +} diff --git a/Stylet/StyletIoC/ICreator.cs b/Stylet/StyletIoC/ICreator.cs new file mode 100644 index 0000000..2d4cd43 --- /dev/null +++ b/Stylet/StyletIoC/ICreator.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIoC +{ + internal interface ICreator + { + Type Type { get; } + Expression GetInstanceExpression(); + } + + internal abstract class CreatorBase : ICreator + { + public virtual Type Type { get; protected set; } + protected StyletIoCContainer container; + public abstract Expression GetInstanceExpression(); + + public CreatorBase(StyletIoCContainer container) + { + this.container = container; + } + + // Common utility method + protected Expression CompleteExpressionFromCreator(Expression creator) + { + var instanceVar = Expression.Variable(this.Type, "instance"); + var assignment = Expression.Assign(instanceVar, creator); + + var buildUpExpression = this.container.GetBuilderUpper(this.Type).GetExpression(instanceVar); + + // We always start with: + // var instance = new Class(.....) + // instance.Property1 = new .... + // instance.Property2 = new .... + var blockItems = new List() { assignment, buildUpExpression }; + // If it implements IInjectionAware, follow that up with: + // instance.ParametersInjected + if (typeof(IInjectionAware).IsAssignableFrom(this.Type)) + blockItems.Add(Expression.Call(instanceVar, typeof(IInjectionAware).GetMethod("ParametersInjected"))); + // Final appearance of instanceVar, as this sets the return value of the block + blockItems.Add(instanceVar); + var completeExpression = Expression.Block(new[] { instanceVar }, blockItems); + return completeExpression; + } + } + + // Sealed so Code Analysis doesn't moan about us setting the virtual Type property + internal sealed class TypeCreator : CreatorBase + { + public string AttributeKey { get; private set; } + private Expression creationExpression; + + public TypeCreator(Type type, StyletIoCContainer container) : base(container) + { + this.Type = type; + + // Use the key from InjectAttribute (if present), and let someone else override it if they want + var attribute = (InjectAttribute)type.GetCustomAttributes(typeof(InjectAttribute), false).FirstOrDefault(); + if (attribute != null) + this.AttributeKey = attribute.Key; + } + + private string KeyForParameter(ParameterInfo parameter) + { + var attributes = parameter.GetCustomAttributes(typeof(InjectAttribute)); + if (attributes == null) + return null; + var attribute = (InjectAttribute)attributes.FirstOrDefault(); + return attribute == null ? null : attribute.Key; + } + + public override Expression GetInstanceExpression() + { + if (this.creationExpression != null) + return this.creationExpression; + + // Find the constructor which has the most parameters which we can fulfill, accepting default values which we can't fulfill + ConstructorInfo ctor; + var ctorsWithAttribute = this.Type.GetConstructors().Where(x => x.GetCustomAttributes(typeof(InjectAttribute), false).Any()).ToList(); + if (ctorsWithAttribute.Count > 1) + { + throw new StyletIoCFindConstructorException(String.Format("Found more than one constructor with [Inject] on type {0}.", this.Type.Name)); + } + else if (ctorsWithAttribute.Count == 1) + { + ctor = ctorsWithAttribute[0]; + var key = ((InjectAttribute)ctorsWithAttribute[0].GetCustomAttribute(typeof(InjectAttribute), false)).Key; + var cantResolve = ctor.GetParameters().Where(p => !this.container.CanResolve(new TypeKey(p.ParameterType, key)) && !p.HasDefaultValue).FirstOrDefault(); + if (cantResolve != null) + throw new StyletIoCFindConstructorException(String.Format("Found a constructor with [Inject] on type {0}, but can't resolve parameter '{1}' (which doesn't have a default value).", this.Type.Name, cantResolve.Name)); + } + else + { + ctor = this.Type.GetConstructors() + .Where(c => c.GetParameters().All(p => this.container.CanResolve(new TypeKey(p.ParameterType, this.KeyForParameter(p))) || p.HasDefaultValue)) + .OrderByDescending(c => c.GetParameters().Count(p => !p.HasDefaultValue)) + .FirstOrDefault(); + + if (ctor == null) + { + throw new StyletIoCFindConstructorException(String.Format("Unable to find a constructor for type {0} which we can call.", this.Type.Name)); + } + } + + // If we get circular dependencies, we'll just blow the stack. They're a pain to resolve. + + // If there parameter's got an InjectAttribute with a key, use that key to resolve + var ctorParams = ctor.GetParameters().Select(x => + { + var key = this.KeyForParameter(x); + if (this.container.CanResolve(new TypeKey(x.ParameterType, key))) + { + try + { + return this.container.GetExpression(new TypeKey(x.ParameterType, key), true); + } + catch (StyletIoCRegistrationException e) + { + throw new StyletIoCRegistrationException(String.Format("{0} Required by paramter '{1}' of type {2}.", e.Message, x.Name, this.Type.Name), e); + } + } + // For some reason we need this cast... + return Expression.Convert(Expression.Constant(x.DefaultValue), x.ParameterType); + }); + + var creator = Expression.New(ctor, ctorParams); + + var completeExpression = this.CompleteExpressionFromCreator(creator); + + this.creationExpression = completeExpression; + return completeExpression; + } + } + + // Sealed for consistency with TypeCreator + internal sealed class FactoryCreator : CreatorBase + { + private Func factory; + private Expression instanceExpression; + + public override Type Type { get { return typeof(T); } } + + public FactoryCreator(Func factory, StyletIoCContainer container) : base(container) + { + this.factory = factory; + } + + public override Expression GetInstanceExpression() + { + if (this.instanceExpression != null) + return this.instanceExpression; + + var expr = (Expression>)(() => this.factory(this.container)); + var invoked = Expression.Invoke(expr, null); + + var completeExpression = this.CompleteExpressionFromCreator(invoked); + + this.instanceExpression = completeExpression; + return completeExpression; + } + } +} diff --git a/Stylet/StyletIoC/IRegistration.cs b/Stylet/StyletIoC/IRegistration.cs new file mode 100644 index 0000000..1b464f1 --- /dev/null +++ b/Stylet/StyletIoC/IRegistration.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StyletIoC +{ + internal interface IRegistration + { + Type Type { get; } + Func GetGenerator(); + Expression GetInstanceExpression(); + } + + internal abstract class RegistrationBase : IRegistration + { + protected ICreator creator; + + public Type Type { get { return this.creator.Type; } } + + protected Func generator { get; set; } + + public RegistrationBase(ICreator creator) + { + this.creator = creator; + } + + public abstract Func GetGenerator(); + public abstract Expression GetInstanceExpression(); + } + + + internal class TransientRegistration : RegistrationBase + { + public TransientRegistration(ICreator creator) : base(creator) { } + + public override Expression GetInstanceExpression() + { + return this.creator.GetInstanceExpression(); + } + + public override Func GetGenerator() + { + if (this.generator == null) + this.generator = Expression.Lambda>(this.GetInstanceExpression()).Compile(); + return this.generator; + } + } + + internal class SingletonRegistration : RegistrationBase + { + private object instance; + private Expression instanceExpression; + + public SingletonRegistration(ICreator creator) : base(creator) { } + + private void EnsureInstantiated() + { + if (this.instance != null) + return; + + // Ensure we don't end up creating two singletons, one used by each thread + Interlocked.CompareExchange(ref this.instance, Expression.Lambda>(this.creator.GetInstanceExpression()).Compile()(), null); + } + + public override Func GetGenerator() + { + this.EnsureInstantiated(); + + if (this.generator == null) + this.generator = () => this.instance; + + return this.generator; + } + + public override Expression GetInstanceExpression() + { + if (this.instanceExpression != null) + return this.instanceExpression; + + this.EnsureInstantiated(); + + // This expression yields the actual type of instance, not 'object' + this.instanceExpression = Expression.Constant(this.instance); + return this.instanceExpression; + } + } + + internal class GetAllRegistration : IRegistration + { + private StyletIoCContainer container; + + public string Key { get; set; } + public Type Type { get; private set; } + + private Expression expression; + private Func generator; + + public GetAllRegistration(Type type, StyletIoCContainer container) + { + this.Type = type; + this.container = container; + } + + public Func GetGenerator() + { + if (this.generator == null) + this.generator = Expression.Lambda>(this.GetInstanceExpression()).Compile(); + return this.generator; + } + + public Expression GetInstanceExpression() + { + if (this.expression != null) + return this.expression; + + var list = Expression.New(this.Type); + var init = Expression.ListInit(list, this.container.GetRegistrations(new TypeKey(this.Type.GenericTypeArguments[0], this.Key), false).GetAll().Select(x => x.GetInstanceExpression())); + + this.expression = init; + return this.expression; + } + } +} diff --git a/Stylet/StyletIoC/IRegistrationCollection.cs b/Stylet/StyletIoC/IRegistrationCollection.cs new file mode 100644 index 0000000..c4b1e31 --- /dev/null +++ b/Stylet/StyletIoC/IRegistrationCollection.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIoC +{ + internal interface IRegistrationCollection + { + IRegistration GetSingle(); + List GetAll(); + IRegistrationCollection AddRegistration(IRegistration registration); + } + + internal class SingleRegistration : IRegistrationCollection + { + private IRegistration registration; + + public SingleRegistration(IRegistration registration) + { + this.registration = registration; + } + + public IRegistration GetSingle() + { + return this.registration; + } + + public List GetAll() + { + return new List() { this.registration }; + } + + public IRegistrationCollection AddRegistration(IRegistration registration) + { + return new RegistrationCollection(new List() { this.registration, registration }); + } + } + + internal class RegistrationCollection : IRegistrationCollection + { + private List registrations; + + public RegistrationCollection(List registrations) + { + this.registrations = registrations; + } + + public IRegistration GetSingle() + { + throw new StyletIoCRegistrationException("Multiple registrations found."); + } + + public List GetAll() + { + List registrationsCopy; + lock (this.registrations) { registrationsCopy = registrations.ToList(); } + return registrationsCopy; + } + + public IRegistrationCollection AddRegistration(IRegistration registration) + { + // Need to lock the list, as someone might be fetching from it while we do this + lock (this.registrations) + { + // Is there an existing registration for this type? + if (this.registrations.Any(x => x.Type == registration.Type)) + throw new StyletIoCRegistrationException(String.Format("Multiple registrations for type {0} found.", registration.Type.Name)); + this.registrations.Add(registration); + return this; + } + } + } +} diff --git a/Stylet/StyletIoC/StyletIoCBuilder.cs b/Stylet/StyletIoC/StyletIoCBuilder.cs new file mode 100644 index 0000000..fd9897a --- /dev/null +++ b/Stylet/StyletIoC/StyletIoCBuilder.cs @@ -0,0 +1,421 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIoC +{ + /// + /// Interface for selecting what to bind a service to. + /// Call StyletIoCBuilder.Bind(..) to get an instance of this + /// + public interface IBindTo + { + /// + /// Bind the specified service to itself - if you self-bind MyClass, and request an instance of MyClass, you'll get an instance of MyClass. + /// + IInScopeOrWithKey ToSelf(); + + /// + /// Bind the specified service to another type which implements that service. E.g. builder.Bind{IMyClass}().To{MyClass}(), and request an IMyClass: you'll get a MyClass. + /// + /// Type to bind the service to + IInScopeOrWithKey To(); + + /// + /// Bind the specified service to another type which implements that service. E.g. builder.Bind{IMyClass}().To(typeof(MyClass)), and request an IMyClass: you'll get a MyClass. + /// + /// Type to bind the service to + IInScopeOrWithKey To(Type implementationType); + + /// + /// Bind the specified service to a factory delegate, which will be called when an instance is required. E.g. ...ToFactory(c => new MyClass(c.Get{Dependency}(), "foo")) + /// + /// Type returned by the factory delegate. Must implement the service + /// Factory delegate to bind got + IInScopeOrWithKey ToFactory(Func factory); + + /// + /// If the service is an interface with a number of methods which return other types, generate an implementation of that abstract factory and bind it to the interface. + /// + IWithKey ToAbstractFactory(); + + /// + /// Discover all implementations of the service in the specified assemblies / the current assembly, and bind those to the service + /// + /// Assemblies to search. If empty / null, searches the current assembly + IInScopeOrWithKey ToAllImplementations(params Assembly[] assemblies); + } + + public interface IInScopeOrWithKey : IInScope + { + /// + /// Associate a key with this binding. Requests for the service will have to specify this key to retrieve the result of this binding + /// + /// Key to associate with this binding + IInScope WithKey(string key); + } + public interface IWithKey + { + /// + /// Associate a key with this binding. Requests for the service will have to specify this key to retrieve the result of this binding + /// + /// Key to associate with this binding + void WithKey(string key); + } + public interface IInScope + { + /// + /// Modify the scope of the binding to Singleton. One instance of this implementation will be generated for this binding. + /// + void InSingletonScope(); + } + + internal class BuilderBindTo : IBindTo + { + public Type ServiceType { get; private set; } + private BuilderBindingBase builderBinding; + public bool IsWeak { get; private set; } + public string Key { get { return this.builderBinding.Key; } } + + public BuilderBindTo(Type serviceType, bool isWeak) + { + this.ServiceType = serviceType; + this.IsWeak = isWeak; + } + + public IInScopeOrWithKey ToSelf() + { + return this.To(this.ServiceType); + } + + public IInScopeOrWithKey To() + { + return this.To(typeof(TImplementation)); + } + + public IInScopeOrWithKey To(Type implementationType) + { + this.builderBinding = new BuilderTypeBinding(this.ServiceType, implementationType); + return this.builderBinding; + } + + public IInScopeOrWithKey ToFactory(Func factory) + { + this.builderBinding = new BuilderFactoryBinding(this.ServiceType, factory); + return this.builderBinding; + } + + public IWithKey ToAbstractFactory() + { + this.builderBinding = new AbstractFactoryBinding(this.ServiceType); + return this.builderBinding; + } + + public IInScopeOrWithKey ToAllImplementations(params Assembly[] assemblies) + { + if (assemblies == null || assemblies.Length == 0) + assemblies = new[] { Assembly.GetCallingAssembly() }; + this.builderBinding = new BuilderToAllImplementationsBinding(this.ServiceType, assemblies); + return this.builderBinding; + } + + internal void Build(StyletIoCContainer container) + { + this.builderBinding.Build(container); + } + } + + internal abstract class BuilderBindingBase : IInScopeOrWithKey, IWithKey + { + protected Type serviceType; + protected bool isSingleton; + public string Key { get; protected set; } + + public BuilderBindingBase(Type serviceType) + { + this.serviceType = serviceType; + } + + void IInScope.InSingletonScope() + { + this.isSingleton = true; + } + + IInScope IInScopeOrWithKey.WithKey(string key) + { + this.Key = key; + return this; + } + + protected void EnsureType(Type implementationType) + { + if (!implementationType.IsClass || implementationType.IsAbstract) + throw new StyletIoCRegistrationException(String.Format("Type {0} is not a concrete class, and so can't be used to implemented service {1}", implementationType.Name, this.serviceType.Name)); + + // Test this first, as it's a bit clearer than hitting 'type doesn't implement service' + if (implementationType.IsGenericTypeDefinition) + { + if (this.isSingleton) + throw new StyletIoCRegistrationException(String.Format("You cannot create singleton registration for unbound generic type {0}", implementationType.Name)); + + if (!this.serviceType.IsGenericTypeDefinition) + throw new StyletIoCRegistrationException(String.Format("You may not bind the unbound generic type {0} to the bound generic / non-generic service {1}", implementationType.Name, this.serviceType.Name)); + + // This restriction may change when I figure out how to pass down the correct type argument + if (this.serviceType.GetTypeInfo().GenericTypeParameters.Length != implementationType.GetTypeInfo().GenericTypeParameters.Length) + throw new StyletIoCRegistrationException(String.Format("If you're registering an unbound generic type to an unbound generic service, both service and type must have the same number of type parameters. Service: {0}, Type: {1}", this.serviceType.Name, implementationType.Name)); + } + else if (this.serviceType.IsGenericTypeDefinition) + { + throw new StyletIoCRegistrationException(String.Format("You cannot bind the bound generic / non-generic type {0} to unbound generic service {1}", implementationType.Name, this.serviceType.Name)); + } + + if (!implementationType.Implements(this.serviceType)) + throw new StyletIoCRegistrationException(String.Format("Type {0} does not implement service {1}", implementationType.Name, this.serviceType.Name)); + } + + // Convenience... + protected void BindImplementationToService(StyletIoCContainer container, Type implementationType, Type serviceType = null) + { + serviceType = serviceType ?? this.serviceType; + + if (this.serviceType.IsGenericTypeDefinition) + { + var unboundGeneric = new UnboundGeneric(implementationType, container, this.isSingleton); + container.AddUnboundGeneric(new TypeKey(serviceType, this.Key), unboundGeneric); + } + else + { + var creator = new TypeCreator(implementationType, container); + IRegistration registration = this.isSingleton ? (IRegistration)new SingletonRegistration(creator) : (IRegistration)new TransientRegistration(creator); + + container.AddRegistration(new TypeKey(this.serviceType, this.Key ?? creator.AttributeKey), registration); + } + } + + void IWithKey.WithKey(string key) + { + this.Key = key; + } + + public abstract void Build(StyletIoCContainer container); + } + + internal class BuilderTypeBinding : BuilderBindingBase + { + private Type implementationType; + + public BuilderTypeBinding(Type serviceType, Type implementationType) : base(serviceType) + { + this.EnsureType(implementationType); + this.implementationType = implementationType; + } + + public override void Build(StyletIoCContainer container) + { + this.BindImplementationToService(container, this.implementationType); + } + } + + internal class BuilderFactoryBinding : BuilderBindingBase + { + private Func factory; + + public BuilderFactoryBinding(Type serviceType, Func factory) : base(serviceType) + { + this.EnsureType(typeof(TImplementation)); + if (this.serviceType.IsGenericTypeDefinition) + throw new StyletIoCRegistrationException(String.Format("A factory cannot be used to implement unbound generic type {0}", this.serviceType.Name)); + this.factory = factory; + } + + public override void Build(StyletIoCContainer container) + { + var creator = new FactoryCreator(this.factory, container); + IRegistration registration = this.isSingleton ? (IRegistration)new SingletonRegistration(creator) : (IRegistration)new TransientRegistration(creator); + + container.AddRegistration(new TypeKey(this.serviceType, this.Key), registration); + } + } + + internal class BuilderToAllImplementationsBinding : BuilderBindingBase + { + private IEnumerable assemblies; + + public BuilderToAllImplementationsBinding(Type serviceType, IEnumerable assemblies) : base(serviceType) + { + this.assemblies = assemblies; + } + + public override void Build(StyletIoCContainer container) + { + var candidates = from type in assemblies.SelectMany(x => x.GetTypes()) + let baseType = type.GetBaseTypesAndInterfaces().FirstOrDefault(x => x == this.serviceType || x.IsGenericType && x.GetGenericTypeDefinition() == this.serviceType) + where baseType != null + select new { Type = type, Base = baseType.ContainsGenericParameters ? baseType.GetGenericTypeDefinition() : baseType }; + + foreach (var candidate in candidates) + { + try + { + this.EnsureType(candidate.Type); + this.BindImplementationToService(container, candidate.Type, candidate.Base); + } + catch (StyletIoCRegistrationException e) + { + Debug.WriteLine(String.Format("Unable to auto-bind type {0} to {1}: {2}", candidate.Base.Name, candidate.Type.Name, e.Message), "StyletIoC"); + } + } + } + } + + internal class AbstractFactoryBinding : BuilderBindingBase + { + public AbstractFactoryBinding(Type serviceType) : base(serviceType) { } + + public override void Build(StyletIoCContainer container) + { + var factoryType = container.GetFactoryForType(this.serviceType); + var creator = new TypeCreator(factoryType, container); + var registration = new SingletonRegistration(creator); + + container.AddRegistration(new TypeKey(this.serviceType, this.Key), registration); + } + } + + /// + /// This IStyletIoCBuilder is the only way to create an IContainer. Binding are registered using the builder, than an IContainer generated. + /// + public interface IStyletIoCBuilder + { + /// + /// Bind the specified service (interface, abstract class, concrete class, unbound generic, etc) to something + /// + /// Service to bind + IBindTo Bind(); + + /// + /// Bind the specified service (interface, abstract class, concrete class, unbound generic, etc) to something + /// + /// Service to bind + IBindTo Bind(Type serviceType); + + /// + /// Search the specified assembly(s) / the current assembly for concrete types, and self-bind them + /// + /// Assembly(s) to search, or leave empty / null to search the current assembly + void Autobind(params Assembly[] assemblies); + + /// + /// Search the specified assembly(s) / the current assembly for concrete types, and self-bind them + /// + /// Assembly(s) to search, or leave empty / null to search the current assembly + void Autobind(IEnumerable assemblies); + + /// + /// Once all bindings have been set, build an IContainer from which instances can be fetches + /// + /// An IContainer, which should be used from now on + IContainer BuildContainer(); + } + + /// + /// This StyletIoCBuilder is the only way to create an IContainer. Binding are registered using the builder, than an IContainer generated. + /// + public class StyletIoCBuilder : IStyletIoCBuilder + { + private List bindings = new List(); + + /// + /// Bind the specified service (interface, abstract class, concrete class, unbound generic, etc) to something + /// + /// Service to bind + public IBindTo Bind(Type serviceType) + { + return this.BindInternal(serviceType, false); + } + + internal IBindTo BindWeak(Type serviceType) + { + return this.BindInternal(serviceType, true); + } + + internal IBindTo BindInternal(Type serviceType, bool isWeak) + { + var builderBindTo = new BuilderBindTo(serviceType, isWeak); + this.bindings.Add(builderBindTo); + return builderBindTo; + } + + /// + /// Bind the specified service (interface, abstract class, concrete class, unbound generic, etc) to something + /// + /// Service to bind + public IBindTo Bind() + { + return this.Bind(typeof(TService)); + } + + /// + /// Search the specified assembly(s) / the current assembly for concrete types, and self-bind them + /// + /// Assembly(s) to search, or leave empty / null to search the current assembly + public void Autobind(IEnumerable assemblies) + { + // If they haven't given any assemblies, use the assembly of the caller + if (assemblies == null || !assemblies.Any()) + assemblies = new[] { Assembly.GetCallingAssembly() }; + + // We self-bind concrete classes only + var classes = assemblies.SelectMany(x => x.GetTypes()).Where(c => c.IsClass && !c.IsAbstract); + foreach (var cls in classes) + { + // Don't care if binding fails - we're likely to hit a few of these + try + { + this.BindWeak(cls).To(cls); + } + catch (StyletIoCRegistrationException e) + { + Debug.WriteLine(String.Format("Unable to auto-bind type {0}: {1}", cls.Name, e.Message), "StyletIoC"); + } + } + } + + /// + /// Search the specified assembly(s) / the current assembly for concrete types, and self-bind them + /// + /// Assembly(s) to search, or leave empty / null to search the current assembly + public void Autobind(params Assembly[] assemblies) + { + // Have to do null-or-empty check here as well, otherwise GetCallingAssembly returns this one.... + if (assemblies == null || assemblies.Length == 0) + assemblies = new[] { Assembly.GetCallingAssembly() }; + this.Autobind(assemblies.AsEnumerable()); + } + + /// + /// Once all bindings have been set, build an IContainer from which instances can be fetches + /// + /// An IContainer, which should be used from now on + public IContainer BuildContainer() + { + var container = new StyletIoCContainer(); + container.AddRegistration(new TypeKey(typeof(IContainer), null), new SingletonRegistration(new FactoryCreator(c => container, container))); + container.AddRegistration(new TypeKey(typeof(StyletIoCContainer), null), new SingletonRegistration(new FactoryCreator(c => container, container))); + + // For each TypeKey, we remove any weak bindings if there are any strong bindings + var groups = this.bindings.GroupBy(x => new { Key = x.Key, Type = x.ServiceType }); + var filtered = groups.SelectMany(group => group.Any(x => !x.IsWeak) ? group.Where(x => !x.IsWeak) : group); + foreach (var binding in filtered) + { + binding.Build(container); + } + return container; + } + } +} diff --git a/Stylet/StyletIoC/StyletIoCContainer.cs b/Stylet/StyletIoC/StyletIoCContainer.cs new file mode 100644 index 0000000..e72c138 --- /dev/null +++ b/Stylet/StyletIoC/StyletIoCContainer.cs @@ -0,0 +1,634 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StyletIoC +{ + public interface IContainer + { + /// + /// Compile all known bindings (which would otherwise be compiled when needed), checking the dependency graph for consistency + /// + /// If true, throw if we fail to compile a type + void Compile(bool throwOnError = true); + + /// + /// Fetch a single instance of the specified type + /// + /// Type of service to fetch an implementation for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// An instance of the requested service + object Get(Type type, string key = null); + + /// + /// Fetch a single instance of the specified type + /// + /// Type of service to fetch an implementation for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// An instance of the requested service + T Get(string key = null); + + /// + /// Fetch instances of all types which implement the specified service + /// + /// Type of the service to fetch implementations for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// All implementations of the requested service, with the requested key + IEnumerable GetAll(Type type, string key = null); + + /// + /// Fetch instances of all types which implement the specified service + /// + /// Type of the service to fetch implementations for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// All implementations of the requested service, with the requested key + IEnumerable GetAll(string key = null); + + /// + /// If type is an IEnumerable{T} or similar, is equivalent to calling GetAll{T}. Else, is equivalent to calling Get{T}. + /// + /// If IEnumerable{T}, will fetch all implementations of T, otherwise wil fetch a single T + /// Key that implementations of the service to fetch were registered with, defaults to null + /// + object GetTypeOrAll(Type type, string key = null); + + /// + /// If type is an IEnumerable{T} or similar, is equivalent to calling GetAll{T}. Else, is equivalent to calling Get{T}. + /// + /// If IEnumerable{T}, will fetch all implementations of T, otherwise wil fetch a single T + /// Key that implementations of the service to fetch were registered with, defaults to null + /// + T GetTypeOrAll(string key = null); + + /// + /// For each property/field with the [Inject] attribute, sets it to an instance of that type + /// + /// Item to build up + void BuildUp(object item); + } + + /// + /// Lightweight, very fast IoC container + /// + // Needs to be public, or FactoryAssemblyName isn't visible + public class StyletIoCContainer : IContainer + { + /// + /// Name of the assembly in which abstract factories are built. Use in [assembly: InternalsVisibleTo(StyletIoC.FactoryAssemblyName)] to allow factories created by .ToAbstractFactory() to access internal types + /// + public static readonly string FactoryAssemblyName = "StyletIoCFactory"; + + /// + /// Maps a [type, key] pair to a collection of registrations for that keypair. You can retrieve an instance of the type from the registration + /// + private readonly ConcurrentDictionary registrations = new ConcurrentDictionary(); + + /// + /// Maps a [type, key] pair, where 'type' is the T in IEnumerable{T}, to a registration which can create a List{T} implementing that IEnumerable. + /// This is separate from 'registrations' as some code paths - e.g. Get() - won't search it (while things like constructor/property injection will). + /// + private readonly ConcurrentDictionary getAllRegistrations = new ConcurrentDictionary(); + + /// + /// Maps a [type, key] pair, where 'type' is an unbound generic (something like IValidator{}) to something which, given a type, can create an IRegistration for that type. + /// So if they've bound an IValidator{} to a an IntValidator, StringValidator, etc, and request an IValidator{string}, one of the UnboundGenerics here can generatr a StringValidator. + /// Type-safety with the List is ensured by locking using the list as the lock object before modifying / iterating. + /// + private readonly ConcurrentDictionary> unboundGenerics = new ConcurrentDictionary>(); + + /// + /// Maps a type onto a BuilderUpper for that type, which can create an Expresson/Delegate to build up that type. + /// + private readonly ConcurrentDictionary builderUppers = new ConcurrentDictionary(); + + /// + /// Cached ModuleBuilder used for building factory implementations + /// + private ModuleBuilder factoryBuilder; + + /// + /// Compile all known bindings (which would otherwise be compiled when needed), checking the dependency graph for consistency + /// + public void Compile(bool throwOnError = true) + { + foreach (var kvp in this.registrations) + { + foreach (var registration in kvp.Value.GetAll()) + { + try + { + registration.GetGenerator(); + } + catch (StyletIoCFindConstructorException) + { + // If they've asked us to be quiet, we will + if (throwOnError) + throw; + } + } + } + } + + /// + /// Fetch a single instance of the specified type + /// + /// Type of service to fetch an implementation for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// An instance of the requested service + public object Get(Type type, string key = null) + { + if (type == null) + throw new ArgumentNullException("type"); + var generator = this.GetRegistrations(new TypeKey(type, key), false).GetSingle().GetGenerator(); + return generator(); + } + + /// + /// Fetch a single instance of the specified type + /// + /// Type of service to fetch an implementation for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// An instance of the requested service + public T Get(string key = null) + { + return (T)this.Get(typeof(T), key); + } + + /// + /// Fetch instances of all types which implement the specified service + /// + /// Type of the service to fetch implementations for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// All implementations of the requested service, with the requested key + public IEnumerable GetAll(Type type, string key = null) + { + if (type == null) + throw new ArgumentNullException("type"); + var typeKey = new TypeKey(type, key); + IRegistration registration; + if (!this.TryRetrieveGetAllRegistrationFromElementType(typeKey, null, out registration)) + throw new StyletIoCRegistrationException(String.Format("Could not find registration for type {0} and key '{1}'", typeKey.Type.Name, typeKey.Key)); + var generator = registration.GetGenerator(); + return (IEnumerable)generator(); + } + + /// + /// Fetch instances of all types which implement the specified service + /// + /// Type of the service to fetch implementations for + /// Key that implementations of the service to fetch were registered with, defaults to null + /// All implementations of the requested service, with the requested key + public IEnumerable GetAll(string key = null) + { + return this.GetAll(typeof(T), key).Cast(); + } + + /// + /// If type is an IEnumerable{T} or similar, is equivalent to calling GetAll{T}. Else, is equivalent to calling Get{T}. + /// + /// If IEnumerable{T}, will fetch all implementations of T, otherwise wil fetch a single T + /// Key that implementations of the service to fetch were registered with, defaults to null + /// + public object GetTypeOrAll(Type type, string key = null) + { + if (type == null) + throw new ArgumentNullException("type"); + var generator = this.GetRegistrations(new TypeKey(type, key), true).GetSingle().GetGenerator(); + return generator(); + } + + /// + /// If type is an IEnumerable{T} or similar, is equivalent to calling GetAll{T}. Else, is equivalent to calling Get{T}. + /// + /// If IEnumerable{T}, will fetch all implementations of T, otherwise wil fetch a single T + /// Key that implementations of the service to fetch were registered with, defaults to null + /// + public T GetTypeOrAll(string key = null) + { + return (T)this.GetTypeOrAll(typeof(T), key); + } + + /// + /// For each property/field with the [Inject] attribute, sets it to an instance of that type + /// + /// Item to build up + public void BuildUp(object item) + { + var builderUpper = this.GetBuilderUpper(item.GetType()); + builderUpper.GetImplementor()(item); + } + + /// + /// Determine whether we can resolve a particular typeKey + /// + /// TypeKey to see if we can resolve + /// Whether the given TypeKey can be resolved + internal bool CanResolve(TypeKey typeKey) + { + IRegistrationCollection registrations; + + if (this.registrations.TryGetValue(typeKey, out registrations) || + this.TryCreateGenericTypesForUnboundGeneric(typeKey, out registrations)) + { + return true; + } + + // Is it a 'get all' request? + IRegistration registration; + return this.TryRetrieveGetAllRegistration(typeKey, out registration); + } + + /// + /// Given a collection type (IEnumerable{T}, etc) extracts the T, or null if we couldn't, or if we can't resolve that [T, key] + /// + /// + /// + private Type GetElementTypeFromCollectionType(TypeKey typeKey) + { + Type type = typeKey.Type; + // Elements are never removed from this.registrations, so we're safe to make this ContainsKey query + if (!type.IsGenericType || type.GenericTypeArguments.Length != 1 || !this.registrations.ContainsKey(new TypeKey(type.GenericTypeArguments[0], typeKey.Key))) + return null; + return type.GenericTypeArguments[0]; + } + + /// + /// Given an type, tries to create or fetch an IRegistration which can create an IEnumerable{T}. If collectionTypeOrNull is given, ensures that the generated + /// implementation of the IEnumerable{T} is compatible with that collection (e.g. if they've requested a List{T} in a constructor param, collectionTypeOrNull will be List{T}). + /// + /// Element type and key to create an IRegistration for + /// If given (not null), ensures that the generated implementation of the collection is compatible with this + /// Returned IRegistration, or null if the method returns false + /// Whether such an IRegistration could be created or retrieved + private bool TryRetrieveGetAllRegistrationFromElementType(TypeKey elementTypeKey, Type collectionTypeOrNull, out IRegistration registration) + { + // TryGet first, as making the generic type is expensive + // If it isn't present, and can be made, GetOrAdd to try and add it, but return the now-existing registration if someone beat us to it + if (this.getAllRegistrations.TryGetValue(elementTypeKey, out registration)) + return true; + + var listType = typeof(List<>).MakeGenericType(elementTypeKey.Type); + if (collectionTypeOrNull != null && !collectionTypeOrNull.IsAssignableFrom(listType)) + return false; + + registration = this.getAllRegistrations.GetOrAdd(elementTypeKey, x => new GetAllRegistration(listType, this) { Key = elementTypeKey.Key }); + return true; + } + + /// + /// Wrapper around TryRetrieveGetAllRegistrationFromElementType, which also extracts the element type from the collection type + /// + /// Type of the collection, and key associated with it + /// Returned IRegistration, or null if the method returns false + /// Whether such an IRegistration could be created or retrieved + private bool TryRetrieveGetAllRegistration(TypeKey typeKey, out IRegistration registration) + { + registration = null; + var elementType = this.GetElementTypeFromCollectionType(typeKey); + if (elementType == null) + return false; + + return this.TryRetrieveGetAllRegistrationFromElementType(new TypeKey(elementType, typeKey.Key), typeKey.Type, out registration); + } + + /// + /// Given a generic type (e.g. IValidator{T}), tries to create a collection of IRegistrations which can implement it from the unbound generic registrations. + /// For example, if someone bound an IValidator{} to Validator{}, and this was called with Validator{T}, the IRegistrationCollection would contain a Validator{T}. + /// + /// + /// + /// + private bool TryCreateGenericTypesForUnboundGeneric(TypeKey typeKey, out IRegistrationCollection registrations) + { + registrations = null; + var type = typeKey.Type; + + if (!type.IsGenericType || type.GenericTypeArguments.Length == 0) + return false; + + Type unboundGenericType = type.GetGenericTypeDefinition(); + + List unboundGenerics; + if (!this.unboundGenerics.TryGetValue(new TypeKey(unboundGenericType, typeKey.Key), out unboundGenerics)) + return false; + + // Need to lock this, as someone might modify the underying list by registering a new unbound generic + lock (unboundGenerics) + { + foreach (var unboundGeneric in unboundGenerics) + { + if (unboundGeneric == null) + continue; + + // Consider this scenario: + // interface IC { } class C : IC { } + // Then they ask for an IC. We need to give them a C + // Search the ancestry of C for an IC (called implOfUnboundGenericType), then create a mapping which says that + // U is a bool and T is an int by comparing this against 'type' - the IC that's registered as the service + // Then use this when making the type for C + + Type newType; + if (unboundGeneric.Type == unboundGenericType) + { + newType = type; + } + else + { + var implOfUnboundGenericType = unboundGeneric.Type.GetBaseTypesAndInterfaces().Single(x => x.Name == unboundGenericType.Name); + var mapping = implOfUnboundGenericType.GenericTypeArguments.Zip(type.GenericTypeArguments, (n, t) => new { Type = t, Name = n }); + + newType = unboundGeneric.Type.MakeGenericType(unboundGeneric.Type.GetTypeInfo().GenericTypeParameters.Select(x => mapping.Single(t => t.Name.Name == x.Name).Type).ToArray()); + } + + if (!type.IsAssignableFrom(newType)) + continue; + + // Right! We've made a new generic type we can use + var registration = unboundGeneric.CreateRegistrationForType(newType); + + // AddRegistration returns the IRegistrationCollection which was added/updated, so the one returned from the final + // call to AddRegistration is the final IRegistrationCollection for this key + registrations = this.AddRegistration(typeKey, registration); + } + } + + return registrations != null; + } + + internal Expression GetExpression(TypeKey typeKey, bool searchGetAllTypes) + { + return this.GetRegistrations(typeKey, searchGetAllTypes).GetSingle().GetInstanceExpression(); + } + + internal IRegistrationCollection GetRegistrations(TypeKey typeKey, bool searchGetAllTypes) + { + IRegistrationCollection registrations; + + // Try to get registrations. If there are none, see if we can add some from unbound generics + if (!this.registrations.TryGetValue(typeKey, out registrations) && + !this.TryCreateGenericTypesForUnboundGeneric(typeKey, out registrations)) + { + if (searchGetAllTypes) + { + // Couldn't find this type - is it a 'get all' collection type? (i.e. they've put IEnumerable in a ctor param) + IRegistration registration; + if (!this.TryRetrieveGetAllRegistration(typeKey, out registration)) + throw new StyletIoCRegistrationException(String.Format("No registrations found for service {0}.", typeKey.Type.Name)); + + // Got this far? Good. There's actually a 'get all' collection type. Proceed with that + registrations = new SingleRegistration(registration); + } + else + { + throw new StyletIoCRegistrationException(String.Format("No registrations found for service {0}.", typeKey.Type.Name)); + } + } + + return registrations; + } + + internal IRegistrationCollection AddRegistration(TypeKey typeKey, IRegistration registration) + { + return this.registrations.AddOrUpdate(typeKey, x => new SingleRegistration(registration), (x, c) => c.AddRegistration(registration)); + } + + internal void AddUnboundGeneric(TypeKey typeKey, UnboundGeneric unboundGeneric) + { + // We're not worried about thread-safety across multiple calls to this function (as it's only called as part of setup, which we're + // not thread-safe about). However someone might be fetching something from this list while we're modifying it, which we need to avoid + var unboundGenerics = this.unboundGenerics.GetOrAdd(typeKey, x => new List()); + lock (unboundGenerics) + { + // Is there an auto-registration for this type? If so, remove it + if (unboundGenerics.Any(x => x.Type == unboundGeneric.Type)) + throw new StyletIoCRegistrationException(String.Format("Multiple registrations for type {0} found", typeKey.Type.Name)); + + unboundGenerics.Add(unboundGeneric); + } + } + + /// + /// + /// Not thread-safe, as it's only ever called from the builder + /// + /// + internal Type GetFactoryForType(Type serviceType) + { + if (!serviceType.IsInterface) + throw new StyletIoCCreateFactoryException(String.Format("Unable to create a factory implementing type {0}, as it isn't an interface", serviceType.Name)); + + if (this.factoryBuilder == null) + { + var assemblyName = new AssemblyName(FactoryAssemblyName); + var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("StyletIoCFactoryModule"); + this.factoryBuilder = moduleBuilder; + } + + // If the service is 'ISomethingFactory', call out new class 'SomethingFactory' + var typeBuilder = this.factoryBuilder.DefineType(serviceType.Name.Substring(1), TypeAttributes.Public); + typeBuilder.AddInterfaceImplementation(serviceType); + + // Define a field which holds a reference to this ioc container + var containerField = typeBuilder.DefineField("container", typeof(IContainer), FieldAttributes.Private); + + // Add a constructor which takes one argument - the container - and sets the field + // public Name(IContainer container) + // { + // this.container = container; + // } + var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(IContainer) }); + var ilGenerator = ctorBuilder.GetILGenerator(); + // Load 'this' and the IOC container onto the stack + ilGenerator.Emit(OpCodes.Ldarg_0); + ilGenerator.Emit(OpCodes.Ldarg_1); + // Store the IOC container in this.container + ilGenerator.Emit(OpCodes.Stfld, containerField); + ilGenerator.Emit(OpCodes.Ret); + + // These are needed by all methods, so get them now + // IContainer.GetTypeOrAll(Type, string) + var containerGetMethod = typeof(IContainer).GetMethod("GetTypeOrAll", new Type[] { typeof(Type), typeof(string) }); + // Type.GetTypeFromHandler(RuntimeTypeHandle) + var typeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle"); + + // Go through each method, emmitting an implementation for each + foreach (var methodInfo in serviceType.GetMethods()) + { + var parameters = methodInfo.GetParameters(); + if (!(parameters.Length == 0 || (parameters.Length == 1 && parameters[0].ParameterType == typeof(string)))) + throw new StyletIoCCreateFactoryException("Can only implement methods with zero arguments, or a single string argument"); + + if (methodInfo.ReturnType == typeof(void)) + throw new StyletIoCCreateFactoryException("Can only implement methods which return something"); + + var attribute = methodInfo.GetCustomAttribute(); + + var methodBuilder = typeBuilder.DefineMethod(methodInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, methodInfo.ReturnType, parameters.Select(x => x.ParameterType).ToArray()); + var methodIlGenerator = methodBuilder.GetILGenerator(); + // Load 'this' onto stack + // Stack: [this] + methodIlGenerator.Emit(OpCodes.Ldarg_0); + // Load value of 'container' field of 'this' onto stack + // Stack: [this.container] + methodIlGenerator.Emit(OpCodes.Ldfld, containerField); + // New local variable which represents type to load + LocalBuilder lb = methodIlGenerator.DeclareLocal(methodInfo.ReturnType); + // Load this onto the stack. This is a RuntimeTypeHandle + // Stack: [this.container, runtimeTypeHandleOfReturnType] + methodIlGenerator.Emit(OpCodes.Ldtoken, lb.LocalType); + // Invoke Type.GetTypeFromHandle with this + // This is equivalent to calling typeof(T) + // Stack: [this.container, typeof(returnType)] + methodIlGenerator.Emit(OpCodes.Call, typeFromHandleMethod); + // Load the given key (if it's a parameter), or the key from the attribute if given, or null, onto the stack + // Stack: [this.container, typeof(returnType), key] + if (parameters.Length == 0) + { + if (attribute == null) + methodIlGenerator.Emit(OpCodes.Ldnull); + else + methodIlGenerator.Emit(OpCodes.Ldstr, attribute.Key); // Load null as the key + } + else + { + methodIlGenerator.Emit(OpCodes.Ldarg_1); // Load the given string as the key + } + // Call container.Get(type, key) + // Stack: [returnedInstance] + methodIlGenerator.Emit(OpCodes.Callvirt, containerGetMethod); + methodIlGenerator.Emit(OpCodes.Ret); + + typeBuilder.DefineMethodOverride(methodBuilder, methodInfo); + } + + Type constructedType; + try + { + constructedType = typeBuilder.CreateType(); + } + catch (TypeLoadException e) + { + throw new StyletIoCCreateFactoryException(String.Format("Unable to create factory type for interface {0}. Ensure that the interface is public, or add [assembly: InternalsVisibleTo(StyletIoC.FactoryAssemblyName)] to your AssemblyInfo.cs", serviceType.Name), e); + } + + return constructedType; + } + + internal BuilderUpper GetBuilderUpper(Type type) + { + return this.builderUppers.GetOrAdd(type, x => new BuilderUpper(type, this)); + } + } + + internal class TypeKey : IEquatable + { + public readonly Type Type; + public readonly string Key; + + public TypeKey(Type type, string key) + { + this.Type = type; + this.Key = key; + } + + public override int GetHashCode() + { + if (this.Key == null) + return this.Type.GetHashCode(); + return this.Type.GetHashCode() ^ this.Key.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (!(obj is TypeKey)) + return false; + return this.Equals((TypeKey)obj); + } + + public bool Equals(TypeKey other) + { + return this.Type == other.Type && this.Key == other.Key; + } + } + + internal static class TypeExtensions + { + public static IEnumerable GetBaseTypesAndInterfaces(this Type type) + { + return type.GetInterfaces().Concat(type.GetBaseTypes()); + } + + public static IEnumerable GetBaseTypes(this Type type) + { + if (type == typeof(object)) + yield break; + var baseType = type.BaseType ?? typeof(object); + + while (baseType != null) + { + yield return baseType; + baseType = baseType.BaseType; + } + } + + public static bool Implements(this Type implementationType, Type serviceType) + { + return serviceType.IsAssignableFrom(implementationType) || + implementationType.GetBaseTypesAndInterfaces().Any(x => x == serviceType || (x.IsGenericType && x.GetGenericTypeDefinition() == serviceType)); + } + } + + public interface IInjectionAware + { + void ParametersInjected(); + } + + public class StyletIoCException : Exception + { + public StyletIoCException(string message) : base(message) { } + public StyletIoCException(string message, Exception innerException) : base(message, innerException) { } + } + + public class StyletIoCRegistrationException : StyletIoCException + { + public StyletIoCRegistrationException(string message) : base(message) { } + public StyletIoCRegistrationException(string message, Exception innerException) : base(message, innerException) { } + } + + public class StyletIoCFindConstructorException : StyletIoCException + { + public StyletIoCFindConstructorException(string message) : base(message) { } + public StyletIoCFindConstructorException(string message, Exception innerException) : base(message, innerException) { } + } + + public class StyletIoCCreateFactoryException : StyletIoCException + { + public StyletIoCCreateFactoryException(string message) : base(message) { } + public StyletIoCCreateFactoryException(string message, Exception innerException) : base(message, innerException) { } + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public sealed class InjectAttribute : Attribute + { + public InjectAttribute() + { + } + + public InjectAttribute(string key) + { + this.Key = key; + } + + // This is a named argument + public string Key { get; set; } + } +} diff --git a/Stylet/StyletIoC/UnboundGeneric.cs b/Stylet/StyletIoC/UnboundGeneric.cs new file mode 100644 index 0000000..5cd1751 --- /dev/null +++ b/Stylet/StyletIoC/UnboundGeneric.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reflection; + +namespace StyletIoC +{ + internal class UnboundGeneric + { + private StyletIoCContainer container; + public string Key { get; set; } + public Type Type { get; private set; } + public int NumTypeParams + { + get { return this.Type.GetTypeInfo().GenericTypeParameters.Length; } + } + public bool IsSingleton { get; private set; } + + public UnboundGeneric(Type type, StyletIoCContainer container, bool isSingleton) + { + this.Type = type; + this.container = container; + } + + public IRegistration CreateRegistrationForType(Type boundType) + { + if (this.IsSingleton) + return new SingletonRegistration(new TypeCreator(boundType, this.container)); + else + return new TransientRegistration(new TypeCreator(boundType, this.container)); + } + } +} diff --git a/Stylet/View.cs b/Stylet/View.cs new file mode 100644 index 0000000..1a3f163 --- /dev/null +++ b/Stylet/View.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Markup; + +namespace Stylet +{ + public class View : DependencyObject + { + private static readonly ContentPropertyAttribute defaultContentProperty = new ContentPropertyAttribute("Content"); + private static IViewManager viewManager; + + static View() + { + // Don't complain that IoC is not initialized if we're in design mode + if (!Execute.InDesignMode) + viewManager = IoC.Get(); + } + + public static object GetActionTarget(DependencyObject obj) + { + return (object)obj.GetValue(ActionTargetProperty); + } + + public static void SetActionTarget(DependencyObject obj, object value) + { + obj.SetValue(ActionTargetProperty, value); + } + + public static readonly DependencyProperty ActionTargetProperty = + DependencyProperty.RegisterAttached("ActionTarget", typeof(object), typeof(View), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); + + public static object GetModel(DependencyObject obj) + { + return (object)obj.GetValue(ModelProperty); + } + + public static void SetModel(DependencyObject obj, object value) + { + obj.SetValue(ModelProperty, value); + } + + public static readonly DependencyProperty ModelProperty = + DependencyProperty.RegisterAttached("Model", typeof(object), typeof(View), new PropertyMetadata(null, (d, e) => viewManager.OnModelChanged(d, e) )); + + + public static void SetContentProperty(DependencyObject targetLocation, UIElement view) + { + var type = targetLocation.GetType(); + var contentProperty = Attribute.GetCustomAttributes(type, true).OfType().FirstOrDefault() ?? defaultContentProperty; + + type.GetProperty(contentProperty.Name).SetValue(targetLocation, view, null); + } + } +} diff --git a/Stylet/ViewManager.cs b/Stylet/ViewManager.cs new file mode 100644 index 0000000..ad31e34 --- /dev/null +++ b/Stylet/ViewManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + /// + /// Responsible for managing views. Locates the correct view, instantiates it, attaches it to its ViewModel correctly, and handles the View.Model attached property + /// + public interface IViewManager + { + /// + /// Called by the View.Model attached property when the ViewModel its bound to changes + void OnModelChanged(DependencyObject targetLocation, DependencyPropertyChangedEventArgs e); + + /// + /// Given an instance of a ViewModel, locate the correct view for it, and instantiate it + /// + /// ViewModel to locate the view for + /// An instance of the correct view + UIElement CreateViewForModel(object model); + + /// + /// Given an instance of a ViewModel and an instance of its View, bind the two together + /// + /// View to bind to the ViewModel + /// ViewModel to bind the View to + void BindViewToModel(UIElement view, object viewModel); + } + + public class ViewManager : IViewManager + { + public virtual void OnModelChanged(DependencyObject targetLocation, DependencyPropertyChangedEventArgs e) + { + if (e.OldValue == e.NewValue) + return; + + if (e.NewValue != null) + { + UIElement view; + var viewModelAsViewAware = e.NewValue as IViewAware; + if (viewModelAsViewAware != null && viewModelAsViewAware.View != null) + { + view = viewModelAsViewAware.View; + } + else + { + view = this.CreateViewForModel(e.NewValue); + this.BindViewToModel(view, e.NewValue); + } + + View.SetContentProperty(targetLocation, view); + } + else + { + View.SetContentProperty(targetLocation, null); + } + } + + public virtual Type LocalViewForModel(Type modelType) + { + var viewName = Regex.Replace(modelType.FullName, @"ViewModel", "View"); + // TODO: This might need some more thinking + var viewType = AssemblySource.Assemblies.SelectMany(x => x.GetExportedTypes()).FirstOrDefault(x => x.FullName == viewName); + + if (viewType == null) + throw new Exception(String.Format("Unable to find a View with type {0}", viewName)); + + return viewType; + } + + public virtual UIElement CreateViewForModel(object model) + { + var viewType = this.LocalViewForModel(model.GetType()); + + if (viewType.IsInterface || viewType.IsAbstract || !typeof(UIElement).IsAssignableFrom(viewType)) + throw new Exception(String.Format("Found type for view: {0}, but it wasn't a class derived from UIElement", viewType.Name)); + + var view = (UIElement)IoC.GetInstance(viewType, null); + + // If it doesn't have a code-behind, this won't be called + var initializer = viewType.GetMethod("InitializeComponent", BindingFlags.Public | BindingFlags.Instance); + if (initializer != null) + initializer.Invoke(view, null); + + return view; + } + + public virtual void BindViewToModel(UIElement view, object viewModel) + { + View.SetActionTarget(view, viewModel); + + var viewAsFrameworkElement = view as FrameworkElement; + if (viewAsFrameworkElement != null) + viewAsFrameworkElement.DataContext = viewModel; + + var viewModelAsViewAware = viewModel as IViewAware; + if (viewModelAsViewAware != null) + viewModelAsViewAware.AttachView(view); + } + } +} diff --git a/Stylet/WeakEventManager.cs b/Stylet/WeakEventManager.cs new file mode 100644 index 0000000..0fb3293 --- /dev/null +++ b/Stylet/WeakEventManager.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Stylet +{ + public interface IWeakEventManager + { + IPropertyChangedBinding BindWeak(TSource source, Expression> selector, Action handler) + where TSource : class, INotifyPropertyChanged; + } + + internal class WeakPropertyBinding : IPropertyChangedBinding where TSource : class, INotifyPropertyChanged + { + // Make sure we don't end up retaining the source + private readonly WeakReference source; + private readonly string propertyName; + private readonly Func valueSelector; + private readonly Action handler; + private readonly Action remover; + + public WeakPropertyBinding(TSource source, Expression> selector, Action handler, Action remover) + { + this.source = new WeakReference(source); + this.propertyName = selector.NameForProperty(); + this.valueSelector = selector.Compile(); + this.handler = handler; + this.remover = remover; + + PropertyChangedEventManager.AddHandler(source, this.PropertyChangedHandler, this.propertyName); + } + + internal void PropertyChangedHandler(object sender, PropertyChangedEventArgs e) + { + TSource source; + if (this.source.TryGetTarget(out source)) + this.handler(this.valueSelector(source)); + else + this.remover(this); + } + + public void Unbind() + { + TSource source; + if (this.source.TryGetTarget(out source)) + PropertyChangedEventManager.RemoveHandler(source, this.PropertyChangedHandler, this.propertyName); + this.remover(this); + } + } + + public class WeakEventManager : IWeakEventManager + { + private object bindingsLock = new object(); + private List bindings = new List(); + + public IPropertyChangedBinding BindWeak(TSource source, Expression> selector, Action handler) + where TSource : class, INotifyPropertyChanged + { + // So, the handler's target might point to the class that owns us, or it might point to a compiler-generated class + // We assume we're owned by whatever determines how long the handler's target should live for + // Therefore we'll retain the handler's target for as long as we're alive (unless it's unregistered) + + // The PropertyChangedEventManager is safe to use with delegates whose targets aren't compiler-generated, so we can + // ensure we provide a delegate with a non-compiler-generated target. + // To do this, we'll create a new WeakPropertyBinding instance, and retain it ourselves (so it lives as long as we do, + // and therefore as long as the thing that owns us does). The PropertyChangedEventManager will have a weak reference to + // the WeakPropertyBinding instance, so once we release it, it will too. + + var propertyName = selector.NameForProperty(); + var compiledSelector = selector.Compile(); + + var binding = new WeakPropertyBinding(source, selector, handler, this.Remove); + lock (this.bindingsLock) + { + this.bindings.Add(binding); + } + + return binding; + } + + internal void Remove(IPropertyChangedBinding binding) + { + lock (this.bindingsLock) + { + this.bindings.Remove(binding); + } + } + } +} diff --git a/Stylet/WindowManager.cs b/Stylet/WindowManager.cs new file mode 100644 index 0000000..f7647cc --- /dev/null +++ b/Stylet/WindowManager.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; +using System.Windows.Navigation; + +namespace Stylet +{ + public interface IWindowManager + { + void ShowWindow(object viewModel); + bool? ShowDialog(object viewModel); + } + + public class WindowManager : IWindowManager + { + public void ShowWindow(object viewModel) + { + this.CreateWindow(viewModel, false).Show(); + } + + public bool? ShowDialog(object viewModel) + { + return this.CreateWindow(viewModel, true).ShowDialog(); + } + + private Window CreateWindow(object viewModel, bool isDialog) + { + var viewManager = IoC.Get(); + + var view = viewManager.CreateViewForModel(viewModel); + var window = view as Window; + if (window == null) + throw new Exception(String.Format("Tried to show {0} as a window, but it isn't a Window", view == null ? "(null)" : view.GetType().Name)); + + viewManager.BindViewToModel(window, viewModel); + + var haveDisplayName = viewModel as IHaveDisplayName; + if (haveDisplayName != null) + { + var binding = new Binding("DisplayName") { Mode = BindingMode.TwoWay }; + window.SetBinding(Window.TitleProperty, binding); + } + + if (isDialog) + { + var owner = this.InferOwnerOf(window); + if (owner != null) + window.Owner = owner; + } + + new WindowConductor(window, viewModel); + + return window; + } + + private Window InferOwnerOf(Window window) + { + if (Application.Current == null) + return null; + + var active = Application.Current.Windows.OfType().Where(x => x.IsActive).FirstOrDefault() ?? Application.Current.MainWindow; + return active == window ? null : active; + } + + class WindowConductor : IChildDelegate + { + private readonly Window window; + private readonly object viewModel; + + public WindowConductor(Window window, object viewModel) + { + this.window = window; + this.viewModel = viewModel; + + // They won't be able to request a close unless they implement IChild anyway... + var viewModelAsChild = this.viewModel as IChild; + if (viewModelAsChild != null) + viewModelAsChild.Parent = this; + + ScreenExtensions.TryActivate(this.viewModel); + + var viewModelAsClose = this.viewModel as IClose; + if (viewModelAsClose != null) + window.Closed += this.WindowClosed; + + if (this.viewModel is IGuardClose) + window.Closing += this.WindowClosing; + + if (this.viewModel is IActivate || this.viewModel is IDeactivate) + window.StateChanged += WindowStateChanged; + } + + void WindowStateChanged(object sender, EventArgs e) + { + switch (this.window.WindowState) + { + case WindowState.Maximized: + case WindowState.Normal: + ScreenExtensions.TryActivate(this.viewModel); + break; + + case WindowState.Minimized: + ScreenExtensions.TryDeactivate(this.viewModel); + break; + } + } + + private void WindowClosed(object sender, EventArgs e) + { + this.window.StateChanged -= this.WindowStateChanged; + this.window.Closed -= this.WindowClosed; + this.window.Closing -= this.WindowClosing; // Not sure this is required + + ScreenExtensions.TryClose(this.viewModel); + } + + /// + /// Closing event from the window + /// + private async void WindowClosing(object sender, CancelEventArgs e) + { + if (e.Cancel) + return; + + // See if the task completed synchronously + var task = ((IGuardClose)this.viewModel).CanCloseAsync(); + if (task.IsCompleted) + { + e.Cancel = !task.Result; + } + else + { + e.Cancel = true; + if (await task) + { + this.window.Closing -= this.WindowClosing; + this.window.StateChanged -= this.WindowStateChanged; + ScreenExtensions.TryClose(this.viewModel); + this.window.Close(); + } + } + } + + /// + /// Close was requested by the child + /// + async void IChildDelegate.CloseItem(object item, bool? dialogResult) + { + if (item != this.viewModel) + return; + + var guardClose = this.viewModel as IGuardClose; + if (guardClose != null && !await guardClose.CanCloseAsync()) + return; + + if (dialogResult != null) + this.window.DialogResult = dialogResult; + + this.window.StateChanged -= this.WindowStateChanged; + this.window.Closed -= this.WindowClosed; + this.window.Closing -= this.WindowClosing; + + ScreenExtensions.TryClose(this.viewModel); + + this.window.Close(); + } + } + } +} diff --git a/Stylet/Xaml/ActionExtension.cs b/Stylet/Xaml/ActionExtension.cs new file mode 100644 index 0000000..b5062cd --- /dev/null +++ b/Stylet/Xaml/ActionExtension.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Windows.Markup; + +namespace Stylet +{ + /// + /// MarkupExtension used for binding Commands and Events to methods on the View.ActionTarget + /// + public class ActionExtension : MarkupExtension + { + /// + /// Name of the method to call + /// + public string Method { get; set; } + + /// + /// Create a new ActionExtension + /// + /// Name of the method to call + public ActionExtension(string method) + { + this.Method = method; + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var valueService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); + + // 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 + if (!(valueService.TargetObject is FrameworkElement)) + return this; + + var propertyAsDependencyProperty = valueService.TargetProperty as DependencyProperty; + if (propertyAsDependencyProperty != null && propertyAsDependencyProperty.PropertyType == typeof(ICommand)) + { + return new CommandAction((FrameworkElement)valueService.TargetObject, this.Method); + } + + var propertyAsEventInfo = valueService.TargetProperty as EventInfo; + if (propertyAsEventInfo != null) + { + var ec = new EventAction((FrameworkElement)valueService.TargetObject, propertyAsEventInfo, this.Method); + return ec.GetDelegate(); + } + + throw new ArgumentException("Can only use ActionExtension with a Command property or an event handler"); + } + } +} diff --git a/Stylet/Xaml/BoolToVisibilityConverter.cs b/Stylet/Xaml/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..fac27e7 --- /dev/null +++ b/Stylet/Xaml/BoolToVisibilityConverter.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace Stylet.Xaml +{ + /// + /// Turn a boolean value into a Visibility + /// + public class BoolToVisibilityConverter : DependencyObject, IValueConverter + { + public static readonly BoolToVisibilityConverter Instance = new BoolToVisibilityConverter(); + + /// + /// Visibility to use if value is true + /// + public Visibility TrueVisibility + { + get { return (Visibility)GetValue(TrueVisibilityProperty); } + set { SetValue(TrueVisibilityProperty, value); } + } + + // Using a DependencyProperty as the backing store for TrueVisibility. This enables animation, styling, binding, etc... + public static readonly DependencyProperty TrueVisibilityProperty = + DependencyProperty.Register("TrueVisibility", typeof(Visibility), typeof(BoolToVisibilityConverter), new PropertyMetadata(Visibility.Visible)); + + /// + /// Visibility to use if value is false + /// + public Visibility FalseVisibility + { + get { return (Visibility)GetValue(FalseVisibilityProperty); } + set { SetValue(FalseVisibilityProperty, value); } + } + + // Using a DependencyProperty as the backing store for FalseVisibility. This enables animation, styling, binding, etc... + public static readonly DependencyProperty FalseVisibilityProperty = + DependencyProperty.Register("FalseVisibility", typeof(Visibility), typeof(BoolToVisibilityConverter), new PropertyMetadata(Visibility.Collapsed)); + + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (!(value is bool)) + return null; + + return (bool)value ? this.TrueVisibility : this.FalseVisibility; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (!(value is Visibility)) + return null; + + var vis = (Visibility)value; + + if (vis == this.TrueVisibility) + return true; + if (vis == this.FalseVisibility) + return false; + return null; + } + } +} diff --git a/Stylet/Xaml/CommandAction.cs b/Stylet/Xaml/CommandAction.cs new file mode 100644 index 0000000..4b5c885 --- /dev/null +++ b/Stylet/Xaml/CommandAction.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +using Expressions = System.Linq.Expressions; + +namespace Stylet +{ + /// + /// ICommand returned by ActionExtension for binding buttons, etc, to methods on a ViewModel. + /// If the method has a parameter, CommandParameter is passed + /// + /// + /// Watches the current View.ActionTarget, and looks for a method with the given name, calling it when the ICommand is called. + /// If a bool property with name Get(methodName) exists, it will be observed and used to enable/disable the ICommand. + /// + public class CommandAction : ICommand + { + /// + /// View to grab the View.ActionTarget from + /// + public FrameworkElement Subject { get; private set; } + + /// + /// Method name. E.g. if someone's gone Buttom Command="{s:Action MyMethod}", this is MyMethod. + /// + public string MethodName { get; private set; } + + /// + /// Generated accessor to grab the value of the guard property, or null if there is none + /// + private Func guardPropertyGetter; + + /// + /// MethodInfo for the method to call. This has to exist, or we throw a wobbly + /// + private MethodInfo targetMethodInfo; + + private object target; + + /// + /// Create a new ActionCommand + /// + /// View to grab the View.ActionTarget from + /// Method name. the MyMethod in Buttom Command="{s:Action MyMethod}". + public CommandAction(FrameworkElement subject, string methodName) + { + this.Subject = subject; + this.MethodName = methodName; + + this.UpdateGuardAndMethod(); + + // Observe the View.ActionTarget for changes, and re-bind the guard property and MethodInfo if it changes + DependencyPropertyDescriptor.FromProperty(View.ActionTargetProperty, typeof(View)).AddValueChanged(this.Subject, (o, e) => this.UpdateGuardAndMethod()); + } + + private string GuardName + { + get { return "Can" + this.MethodName; } + } + + private void UpdateGuardAndMethod() + { + var newTarget = View.GetActionTarget(this.Subject); + MethodInfo targetMethodInfo = null; + + this.guardPropertyGetter = null; + if (newTarget != null) + { + var newTargetType = newTarget.GetType(); + + var guardPropertyInfo = newTargetType.GetProperty(this.GuardName); + if (guardPropertyInfo != null && guardPropertyInfo.PropertyType == typeof(bool)) + { + var targetExpression = Expressions.Expression.Constant(newTarget); + var propertyAccess = Expressions.Expression.Property(targetExpression, guardPropertyInfo); + this.guardPropertyGetter = Expressions.Expression.Lambda>(propertyAccess).Compile(); + } + + targetMethodInfo = newTargetType.GetMethod(this.MethodName); + if (targetMethodInfo == null) + throw new ArgumentException(String.Format("Unable to find method {0} on {1}", this.MethodName, newTargetType.Name)); + + var methodParameters = targetMethodInfo.GetParameters(); + if (methodParameters.Length > 1) + throw new ArgumentException(String.Format("Method {0} on {1} must have zero or one parameters", this.MethodName, newTargetType.Name)); + } + + var oldTarget = this.target as INotifyPropertyChanged; + if (oldTarget != null) + oldTarget.PropertyChanged -= this.PropertyChangedHandler; + + this.target = newTarget; + + var inpc = newTarget as INotifyPropertyChanged; + if (this.guardPropertyGetter != null && inpc != null) + inpc.PropertyChanged += this.PropertyChangedHandler; + + this.targetMethodInfo = targetMethodInfo; + + this.UpdateCanExecute(); + } + + private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e) + { + if (String.IsNullOrEmpty(e.PropertyName) || e.PropertyName == this.GuardName) + { + this.UpdateCanExecute(); + } + } + + private void UpdateCanExecute() + { + var handler = this.CanExecuteChanged; + if (handler != null) + handler(this, EventArgs.Empty); + } + + public bool CanExecute(object parameter) + { + if (this.target == null) + return false; + + if (this.guardPropertyGetter == null) + return true; + + return this.guardPropertyGetter(); + } + + public event EventHandler CanExecuteChanged; + + public void Execute(object parameter) + { + // This is not going to be called very often, so don't bother to generate a delegate, in the way that we do for the method guard + if (this.target == null) + throw new ArgumentException("Target not set"); + + var parameters = this.targetMethodInfo.GetParameters().Length == 1 ? new[] { parameter } : null; + this.targetMethodInfo.Invoke(this.target, parameters); + } + } +} diff --git a/Stylet/Xaml/DebugConverter.cs b/Stylet/Xaml/DebugConverter.cs new file mode 100644 index 0000000..c700144 --- /dev/null +++ b/Stylet/Xaml/DebugConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace Stylet.Xaml +{ + /// + /// Converter which passes through values, but uses Debug.WriteLine to log them. Useful for debugging + /// + public class DebugConverter : DependencyObject, IValueConverter + { + public static readonly DebugConverter Instance = new DebugConverter(); + + /// + /// Category to use with Debug.WriteLine + /// + public string Name + { + get { return (string)GetValue(NameProperty); } + set { SetValue(NameProperty, value); } + } + + // Using a DependencyProperty as the backing store for Name. This enables animation, styling, binding, etc... + public static readonly DependencyProperty NameProperty = + DependencyProperty.Register("Name", typeof(string), typeof(DebugConverter), new PropertyMetadata("DebugConverter")); + + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (parameter == null) + Debug.WriteLine(String.Format("Convert: Value = '{0}' TargetType = '{1}'", value, targetType), this.Name); + else + Debug.WriteLine(String.Format("Convert: Value = '{0}' TargetType = '{1}' Parameter = '{2}'", value, targetType, parameter), this.Name); + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (parameter == null) + Debug.WriteLine(String.Format("ConvertBack: Value = '{0}' TargetType = '{1}'", value, targetType), this.Name); + else + Debug.WriteLine(String.Format("ConvertBack: Value = '{0}' TargetType = '{1}' Parameter = '{2}'", value, targetType, parameter), this.Name); + + return value; + } + } +} diff --git a/Stylet/Xaml/EventAction.cs b/Stylet/Xaml/EventAction.cs new file mode 100644 index 0000000..ddb80e4 --- /dev/null +++ b/Stylet/Xaml/EventAction.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Stylet +{ + /// + /// Created by ActionExtension, this can return a delegate suitable adding binding to an event, and can call a method on the View.ActionTarget + /// + public class EventAction + { + /// + /// View whose View.ActionTarget we watch + /// + private FrameworkElement subject; + + /// + /// Property on the WPF element we're returning a delegate for + /// + private EventInfo targetProperty; + + /// + /// The MyMethod in {s:Action MyMethod}, this is what we call when the event's fired + /// + private string methodName; + + /// + /// MethodInfo for the method to call. This has to exist, or we throw a wobbly + /// + private MethodInfo targetMethodInfo; + + private object target; + + /// + /// Create a new EventAction + /// + /// View whose View.ActionTarget we watch + /// Property on the WPF element we're returning a delegate for + /// The MyMethod in {s:Action MyMethod}, this is what we call when the event's fired + public EventAction(FrameworkElement subject, EventInfo targetProperty, string methodName) + { + this.subject = subject; + this.targetProperty = targetProperty; + this.methodName = methodName; + + // Observe the View.ActionTarget for changes, and re-bind the guard property and MethodInfo if it changes + DependencyPropertyDescriptor.FromProperty(View.ActionTargetProperty, typeof(View)).AddValueChanged(this.subject, (o, e) => this.UpdateMethod()); + } + + private void UpdateMethod() + { + var newTarget = View.GetActionTarget(this.subject); + MethodInfo targetMethodInfo = null; + + if (newTarget != null) + { + var newTargetType = newTarget.GetType(); + targetMethodInfo = newTargetType.GetMethod(this.methodName); + if (targetMethodInfo == null) + throw new ArgumentException(String.Format("Unable to find method {0} on {1}", this.methodName, newTargetType.Name)); + + var methodParameters = targetMethodInfo.GetParameters(); + if (methodParameters.Length > 1 || (methodParameters.Length == 1 && !methodParameters[0].ParameterType.IsAssignableFrom(typeof(RoutedEventArgs)))) + throw new ArgumentException(String.Format("Method {0} on {1} must have zero parameters, or a single parameter accepting a RoutedEventArgs", this.methodName, newTargetType.Name)); + } + + this.target = newTarget; + this.targetMethodInfo = targetMethodInfo; + } + + /// + /// Return a delegate which can be added to the targetProperty + /// + public Delegate GetDelegate() + { + var methodInfo = this.GetType().GetMethod("InvokeCommand", BindingFlags.NonPublic | BindingFlags.Instance); + + var parameterType = this.targetProperty.EventHandlerType; + return Delegate.CreateDelegate(parameterType, this, methodInfo); + } + + private void InvokeCommand(object sender, RoutedEventArgs e) + { + if (this.target == null) + return; + + var parameters = this.targetMethodInfo.GetParameters().Length == 1 ? new object[] { e } : null; + this.targetMethodInfo.Invoke(this.target, parameters); + } + } +} diff --git a/Stylet/Xaml/StyletResourceDictionary.xaml b/Stylet/Xaml/StyletResourceDictionary.xaml new file mode 100644 index 0000000..e987bde --- /dev/null +++ b/Stylet/Xaml/StyletResourceDictionary.xaml @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/StyletIcon.png b/StyletIcon.png new file mode 100644 index 0000000..f8a5c00 Binary files /dev/null and b/StyletIcon.png differ diff --git a/StyletIcon.svg b/StyletIcon.svg new file mode 100644 index 0000000..a107b97 --- /dev/null +++ b/StyletIcon.svg @@ -0,0 +1,98 @@ + +image/svg+xml + +S +S + \ No newline at end of file diff --git a/StyletIntegrationTests/Actions/ActionsView.xaml b/StyletIntegrationTests/Actions/ActionsView.xaml new file mode 100644 index 0000000..611bb28 --- /dev/null +++ b/StyletIntegrationTests/Actions/ActionsView.xaml @@ -0,0 +1,30 @@ + + + + + Verify that the checkbox enables/disables the button, and that clicking the button shows a message box, containing the parameter in the textbox. + + + + Enabled + Parameter + + + + + + + Verify that both buttons show message boxes. + + + + + + + + + diff --git a/StyletIntegrationTests/Actions/ActionsViewModel.cs b/StyletIntegrationTests/Actions/ActionsViewModel.cs new file mode 100644 index 0000000..e8dd316 --- /dev/null +++ b/StyletIntegrationTests/Actions/ActionsViewModel.cs @@ -0,0 +1,50 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace StyletIntegrationTests.Actions +{ + public class ActionsViewModel : Screen + { + private bool _checkboxIsChecked; + public bool CheckboxIsChecked + { + get { return this._checkboxIsChecked; } + set + { + this.SetAndNotify(ref this._checkboxIsChecked, value); + this.NotifyOfPropertyChange(() => this.CanCommandButton); + } + } + + public ActionsViewModel() + { + this.DisplayName = "Actions"; + } + + public bool CanCommandButton + { + get { return this.CheckboxIsChecked; } + } + public void CommandButton(string parameter) + { + MessageBox.Show(String.Format("Parameter is '{0}'", parameter)); + } + + public void EventButtonNoArgs() + { + MessageBox.Show("Success!"); + } + + public void EventButtonWithArgs(RoutedEventArgs e) + { + string buttonText = (string)(((Button)e.Source).Content); + MessageBox.Show(buttonText == "Button 2" ? "Success!" : "Fail: Sender was not sent successfully"); + } + } +} diff --git a/StyletIntegrationTests/App.config b/StyletIntegrationTests/App.config new file mode 100644 index 0000000..8e15646 --- /dev/null +++ b/StyletIntegrationTests/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/StyletIntegrationTests/App.xaml b/StyletIntegrationTests/App.xaml new file mode 100644 index 0000000..1f27463 --- /dev/null +++ b/StyletIntegrationTests/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/StyletIntegrationTests/Bootstrapper.cs b/StyletIntegrationTests/Bootstrapper.cs new file mode 100644 index 0000000..5af4aa6 --- /dev/null +++ b/StyletIntegrationTests/Bootstrapper.cs @@ -0,0 +1,13 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIntegrationTests +{ + public class Bootstrapper : Bootstrapper + { + } +} diff --git a/StyletIntegrationTests/Properties/AssemblyInfo.cs b/StyletIntegrationTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1977897 --- /dev/null +++ b/StyletIntegrationTests/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("StyletIntegrationTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Antony Male")] +[assembly: AssemblyProduct("StyletIntegrationTests")] +[assembly: AssemblyCopyright("Copyright © Antony Male 2014")] +[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/StyletIntegrationTests/Properties/Resources.Designer.cs b/StyletIntegrationTests/Properties/Resources.Designer.cs new file mode 100644 index 0000000..717b0fd --- /dev/null +++ b/StyletIntegrationTests/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace StyletIntegrationTests.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("StyletIntegrationTests.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/StyletIntegrationTests/Properties/Resources.resx b/StyletIntegrationTests/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/StyletIntegrationTests/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/StyletIntegrationTests/Properties/Settings.Designer.cs b/StyletIntegrationTests/Properties/Settings.Designer.cs new file mode 100644 index 0000000..50a6f37 --- /dev/null +++ b/StyletIntegrationTests/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18444 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace StyletIntegrationTests.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/StyletIntegrationTests/Properties/Settings.settings b/StyletIntegrationTests/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/StyletIntegrationTests/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/StyletIntegrationTests/ShellView.xaml b/StyletIntegrationTests/ShellView.xaml new file mode 100644 index 0000000..77ba1ea --- /dev/null +++ b/StyletIntegrationTests/ShellView.xaml @@ -0,0 +1,31 @@ + + + + + + Result: + + + + + + + + + + + + + + + + + + + + + diff --git a/StyletIntegrationTests/ShellViewModel.cs b/StyletIntegrationTests/ShellViewModel.cs new file mode 100644 index 0000000..98ebe81 --- /dev/null +++ b/StyletIntegrationTests/ShellViewModel.cs @@ -0,0 +1,59 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIntegrationTests +{ + public class ShellViewModel : Screen + { + private IWindowManager windowManager; + + public ShellViewModel(IWindowManager windowManager) + { + this.windowManager = windowManager; + + this.DisplayName = "ShellViewModel"; + } + + + private bool? _showDialogAndDialogResultDialogResult; + public bool? ShowDialogAndDialogResultDialogResult + { + get { return this._showDialogAndDialogResultDialogResult; } + set { SetAndNotify(ref this._showDialogAndDialogResultDialogResult, value); } + } + + public void ShowDialogAndDialogResult() + { + var dialog = new ShowDialogAndDialogResult.DialogViewModel(); + this.ShowDialogAndDialogResultDialogResult = this.windowManager.ShowDialog(dialog); + } + + public void ShowWindowsDisplayNameBound() + { + var window = new WindowDisplayNameBound.WindowViewModel(); + this.windowManager.ShowWindow(window); + } + + public void ShowWindowGuardClose() + { + var window = new WindowGuardClose.WindowViewModel(); + this.windowManager.ShowWindow(window); + } + + public void ShowWindowLifecycle() + { + var window = new WindowLifecycle.WindowViewModel(); + this.windowManager.ShowWindow(window); + } + + public void ShowActions() + { + var window = new Actions.ActionsViewModel(); + this.windowManager.ShowDialog(window); + } + } +} diff --git a/StyletIntegrationTests/ShowDialogAndDialogResult/DialogView.xaml b/StyletIntegrationTests/ShowDialogAndDialogResult/DialogView.xaml new file mode 100644 index 0000000..d71f2c6 --- /dev/null +++ b/StyletIntegrationTests/ShowDialogAndDialogResult/DialogView.xaml @@ -0,0 +1,14 @@ + + + Choose the desired DialogResult, then close the dialog. + + + + + + + diff --git a/StyletIntegrationTests/ShowDialogAndDialogResult/DialogViewModel.cs b/StyletIntegrationTests/ShowDialogAndDialogResult/DialogViewModel.cs new file mode 100644 index 0000000..3b2ba05 --- /dev/null +++ b/StyletIntegrationTests/ShowDialogAndDialogResult/DialogViewModel.cs @@ -0,0 +1,32 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIntegrationTests.ShowDialogAndDialogResult +{ + public class DialogViewModel : Screen + { + public BindableCollection> DesiredResult { get; private set; } + public LabelledValue SelectedDesiredResult { get; set; } + + public DialogViewModel() + { + this.DisplayName = "ShowDialog and DialogResult"; + + this.DesiredResult = new BindableCollection>() + { + new LabelledValue("True", true), + new LabelledValue("False", false), + }; + this.SelectedDesiredResult = this.DesiredResult[0]; + } + + public void Close() + { + this.TryClose(this.SelectedDesiredResult.Value); + } + } +} diff --git a/StyletIntegrationTests/StyletIntegrationTests.csproj b/StyletIntegrationTests/StyletIntegrationTests.csproj new file mode 100644 index 0000000..6fbdcae --- /dev/null +++ b/StyletIntegrationTests/StyletIntegrationTests.csproj @@ -0,0 +1,131 @@ + + + + + Debug + AnyCPU + {C6021258-7CAE-4BC2-A08E-8433019405B7} + WinExe + Properties + StyletIntegrationTests + StyletIntegrationTests + v4.5 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/StyletIntegrationTests/WindowDisplayNameBound/WindowView.xaml b/StyletIntegrationTests/WindowDisplayNameBound/WindowView.xaml new file mode 100644 index 0000000..a8adc7b --- /dev/null +++ b/StyletIntegrationTests/WindowDisplayNameBound/WindowView.xaml @@ -0,0 +1,10 @@ + + + Press the button, and verify that the count in the window title increases. When you are done, close the window. + + + diff --git a/StyletIntegrationTests/WindowDisplayNameBound/WindowViewModel.cs b/StyletIntegrationTests/WindowDisplayNameBound/WindowViewModel.cs new file mode 100644 index 0000000..ae92826 --- /dev/null +++ b/StyletIntegrationTests/WindowDisplayNameBound/WindowViewModel.cs @@ -0,0 +1,25 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIntegrationTests.WindowDisplayNameBound +{ + public class WindowViewModel : Screen + { + private int count = 0; + + public WindowViewModel() + { + this.DisplayName = String.Format("Count: {0}", this.count); + } + + public void AddCount() + { + this.count++; + this.DisplayName = String.Format("Count: {0}", this.count); + } + } +} diff --git a/StyletIntegrationTests/WindowGuardClose/WindowView.xaml b/StyletIntegrationTests/WindowGuardClose/WindowView.xaml new file mode 100644 index 0000000..3305fe5 --- /dev/null +++ b/StyletIntegrationTests/WindowGuardClose/WindowView.xaml @@ -0,0 +1,11 @@ + + + Leave the checkbox unchecked, then close the window using the red X at the top. It should not close. + Check the checkbox, then close the window again. It should close after 3 seconds. + Allow Close + + diff --git a/StyletIntegrationTests/WindowGuardClose/WindowViewModel.cs b/StyletIntegrationTests/WindowGuardClose/WindowViewModel.cs new file mode 100644 index 0000000..ac3fb26 --- /dev/null +++ b/StyletIntegrationTests/WindowGuardClose/WindowViewModel.cs @@ -0,0 +1,24 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletIntegrationTests.WindowGuardClose +{ + public class WindowViewModel : Screen + { + public bool AllowClose { get; set; } + + public WindowViewModel() + { + this.DisplayName = "Window Guard Close"; + } + + public override Task CanCloseAsync() + { + return this.AllowClose ? Task.Delay(2000).ContinueWith(t => true) : Task.FromResult(false); + } + } +} diff --git a/StyletIntegrationTests/WindowLifecycle/WindowView.xaml b/StyletIntegrationTests/WindowLifecycle/WindowView.xaml new file mode 100644 index 0000000..a4c19d6 --- /dev/null +++ b/StyletIntegrationTests/WindowLifecycle/WindowView.xaml @@ -0,0 +1,12 @@ + + + Check that the log contains an 'Initial Activate', an 'Activated', and a 'View Loaded' line. + Minimize/maximize the window. Check the log contains a 'Deactivated' and an 'Activated' line. + Close the window. Check that a dialog appears saying 'Closed'. + + + + diff --git a/StyletIntegrationTests/WindowLifecycle/WindowViewModel.cs b/StyletIntegrationTests/WindowLifecycle/WindowViewModel.cs new file mode 100644 index 0000000..b1f0599 --- /dev/null +++ b/StyletIntegrationTests/WindowLifecycle/WindowViewModel.cs @@ -0,0 +1,47 @@ +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace StyletIntegrationTests.WindowLifecycle +{ + public class WindowViewModel : Screen + { + public BindableCollection Log { get; private set; } + + public WindowViewModel() + { + this.DisplayName = "Window Lifecycle"; + + this.Log = new BindableCollection(); + } + + protected override void OnViewLoaded() + { + this.Log.Add("View Loaded"); + } + + protected override void OnInitialActivate() + { + this.Log.Add("Initial Activate"); + } + + protected override void OnActivate() + { + this.Log.Add("Activated"); + } + + protected override void OnDeactivate() + { + this.Log.Add("Deactivated"); + } + + protected override void OnClose() + { + MessageBox.Show("Closed"); + } + } +} diff --git a/StyletUnitTests/ActionExtensionTests.cs b/StyletUnitTests/ActionExtensionTests.cs new file mode 100644 index 0000000..79fb151 --- /dev/null +++ b/StyletUnitTests/ActionExtensionTests.cs @@ -0,0 +1,74 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Markup; + +namespace StyletUnitTests +{ + [TestFixture, RequiresSTA] + public class ActionExtensionTests + { + private ActionExtension actionExtension; + private Mock provideValueTarget; + private Mock serviceProvider; + + [SetUp] + public void SetUp() + { + this.actionExtension = new ActionExtension("MethodName"); + + this.provideValueTarget = new Mock(); + this.provideValueTarget.Setup(x => x.TargetObject).Returns(new FrameworkElement()); + + this.serviceProvider = new Mock(); + serviceProvider.Setup(x => x.GetService(typeof(IProvideValueTarget))).Returns(provideValueTarget.Object); + } + + [Test] + public void ReturnsThisIfTargetObjectIsNotFrameworkElement() + { + this.provideValueTarget.Setup(x => x.TargetObject).Returns(null); + + Assert.AreEqual(this.actionExtension, this.actionExtension.ProvideValue(this.serviceProvider.Object)); + } + + [Test] + public void ReturnsCommandActionIfTargetObjectPropertyTypeIsICommand() + { + // This will be asked for the IViewManager, which we don't care about for now + IoC.GetInstance = (t, k) => null; + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(Button.CommandProperty); + + object value = this.actionExtension.ProvideValue(this.serviceProvider.Object); + Assert.IsInstanceOf(value); + + var action = (CommandAction)value; + + Assert.AreEqual(action.Subject, this.provideValueTarget.Object.TargetObject); + Assert.AreEqual("MethodName", action.MethodName); + } + + [Test] + public void ReturnsEventActionIfTargetObjectPropertyIsEventInfo() + { + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(typeof(Button).GetEvent("Click")); + + Assert.IsInstanceOf(this.actionExtension.ProvideValue(this.serviceProvider.Object)); + } + + [Test] + public void ThrowsArgumentExceptionIfTargetObjectNotDependencyPropertyOrEventInfo() + { + this.provideValueTarget.Setup(x => x.TargetProperty).Returns(5); + + Assert.Throws(() => this.actionExtension.ProvideValue(this.serviceProvider.Object)); + } + } +} diff --git a/StyletUnitTests/BindableCollectionTests.cs b/StyletUnitTests/BindableCollectionTests.cs new file mode 100644 index 0000000..f3b0ca7 --- /dev/null +++ b/StyletUnitTests/BindableCollectionTests.cs @@ -0,0 +1,132 @@ +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class BindableCollectionTests + { + private class Element { } + + [Test] + public void AddRangeAddsElements() + { + var itemsToAdd = new[] { new Element(), new Element() }; + var existingItems = new[] { new Element() }; + var collection = new BindableCollection(existingItems); + + collection.AddRange(itemsToAdd); + + Assert.AreEqual(existingItems.Concat(itemsToAdd), collection); + } + + [Test] + public void RemoveRangeRemovesElements() + { + var itemsToRemove = new[] { new Element(), new Element() }; + var existingItems = new[] { new Element() }; + var collection = new BindableCollection(itemsToRemove.Concat(existingItems)); + + collection.RemoveRange(itemsToRemove); + + Assert.AreEqual(existingItems, collection); + } + + [Test] + public void AddRangeFiresPropertyChanged() + { + var collection = new BindableCollection(new[] { new Element(), new Element() }); + + var changedProperties = new List(); + ((INotifyPropertyChanged)collection).PropertyChanged += (o, e) => changedProperties.Add(e.PropertyName); + + collection.AddRange(new[] { new Element(), new Element() }); + + Assert.That(changedProperties, Is.EquivalentTo(new[] { "Count", "Item[]" })); + } + + [Test] + public void AddRangeFiresCollectionChanged() + { + var collection = new BindableCollection(new[] { new Element(), new Element() }); + + var changedEvents = new List(); + collection.CollectionChanged += (o, e) => changedEvents.Add(e); + + var elementsToAdd = new[] { new Element(), new Element() }; + collection.AddRange(elementsToAdd); + + Assert.AreEqual(1, changedEvents.Count); + var changedEvent = changedEvents[0]; + Assert.AreEqual(NotifyCollectionChangedAction.Add, changedEvent.Action); + Assert.AreEqual(elementsToAdd, changedEvent.NewItems); + } + + [Test] + public void RemoveRangeFiresPropertyChanged() + { + var itemsToRemove = new[] { new Element(), new Element() }; + var collection = new BindableCollection(new[] { new Element() }.Concat(itemsToRemove)); + + var changedProperties = new List(); + ((INotifyPropertyChanged)collection).PropertyChanged += (o, e) => changedProperties.Add(e.PropertyName); + + collection.RemoveRange(itemsToRemove); + + Assert.That(changedProperties, Is.EquivalentTo(new[] { "Count", "Item[]" })); + } + + [Test] + public void RemoveRangeFiresCollectionChanged() + { + var itemsToRemove = new[] { new Element(), new Element() }; + var collection = new BindableCollection(new[] { new Element() }.Concat(itemsToRemove)); + + var changedEvents = new List(); + collection.CollectionChanged += (o, e) => changedEvents.Add(e); + + collection.AddRange(itemsToRemove); + + Assert.AreEqual(1, changedEvents.Count); + var changedEvent = changedEvents[0]; + Assert.AreEqual(NotifyCollectionChangedAction.Add, changedEvent.Action); + Assert.AreEqual(itemsToRemove, changedEvent.NewItems); + } + + [Test] + public void RefreshFiresPropertyChanged() + { + var collection = new BindableCollection(new[] { new Element() }); + + var changedProperties = new List(); + ((INotifyPropertyChanged)collection).PropertyChanged += (o, e) => changedProperties.Add(e.PropertyName); + + collection.Refresh(); + + Assert.That(changedProperties, Is.EquivalentTo(new[] { "Count", "Item[]" })); + } + + [Test] + public void RefreshFiresCollectionChanged() + { + var collection = new BindableCollection(new[] { new Element() }); + + var changedEvents = new List(); + collection.CollectionChanged += (o, e) => changedEvents.Add(e); + + collection.Refresh(); + + Assert.AreEqual(1, changedEvents.Count); + var changedEvent = changedEvents[0]; + Assert.AreEqual(NotifyCollectionChangedAction.Reset, changedEvent.Action); + } + } +} diff --git a/StyletUnitTests/ConductorAllActiveTests.cs b/StyletUnitTests/ConductorAllActiveTests.cs new file mode 100644 index 0000000..6a1dbf2 --- /dev/null +++ b/StyletUnitTests/ConductorAllActiveTests.cs @@ -0,0 +1,162 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class ConductorAllActiveTests + { + private Conductor.Collections.AllActive conductor; + + [SetUp] + public void SetUp() + { + this.conductor = new Conductor.Collections.AllActive(); + } + + [Test] + public void NoActiveItemsBeforeAnyItemsActivated() + { + Assert.IsEmpty(this.conductor.Items); + } + + [Test] + public void ActivateItemAddsItemToItems() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void ActivateItemDoesNotActivateIfConductorIsNotActive() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void ActivateItemDoesActiveIfConductorIsActive() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate()); + } + + [Test] + public void DeactiveDeactivatesItems() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + ((IDeactivate)this.conductor).Deactivate(); + screen.Verify(x => x.Deactivate()); + } + + [Test] + public void ClosingClosesAllItems() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + ((IClose)this.conductor).Close(); + screen.Verify(x => x.Close()); + Assert.AreEqual(0, this.conductor.Items.Count); + } + + [Test] + public void ConductorCanCloseIfAllItemsCanClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object }); + Assert.IsTrue(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void ConductorCanNotCloseIfAnyItemCanNotClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(false)); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object }); + Assert.IsFalse(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void AddingItemActivatesAndSetsParent() + { + ((IActivate)this.conductor).Activate(); + var screen = new Mock(); + this.conductor.Items.Add(screen.Object); + screen.VerifySet(x => x.Parent = this.conductor); + screen.Verify(x => x.Activate()); + } + + [Test] + public void RemovingItemClosesAndRemovesParent() + { + var screen = new Mock(); + screen.SetupGet(x => x.Parent).Returns(this.conductor); + this.conductor.Items.Add(screen.Object); + this.conductor.Items.Remove(screen.Object); + screen.VerifySet(x => x.Parent = null); + screen.Verify(x => x.Close()); + } + + [Test] + public void AddingItemTwiceDoesNotResultInDuplicates() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.ActivateItem(screen.Object); + Assert.AreEqual(1, this.conductor.Items.Count); + } + + [Test] + public void DeactivateItemDeactivatesItemButDoesNotRemoveFromItems() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.DeactivateItem(screen.Object); + screen.Verify(x => x.Deactivate()); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void CloseItemDoesNothingIfItemCanNotClose() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.Verify(x => x.Close(), Times.Never); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void CloseItemDeactivatesItemAndRemovesFromItemsIfItemCanClose() + { + var screen = new Mock(); + screen.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.Verify(x => x.Close()); + Assert.AreEqual(0, this.conductor.Items.Count); + } + } +} diff --git a/StyletUnitTests/ConductorNavigatingTests.cs b/StyletUnitTests/ConductorNavigatingTests.cs new file mode 100644 index 0000000..8d91512 --- /dev/null +++ b/StyletUnitTests/ConductorNavigatingTests.cs @@ -0,0 +1,196 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class ConductorNavigatingTests + { + private Conductor.Collections.Navigation conductor; + + [SetUp] + public void SetUp() + { + this.conductor = new Conductor.Collections.Navigation(); + } + + [Test] + public void ActiveItemIsNullBeforeAnyItemsActivated() + { + Assert.IsNull(this.conductor.ActiveItem); + Assert.That(this.conductor.GetChildren(), Is.EquivalentTo(new IScreen[] { null })); + } + + [Test] + public void InitialActivateSetsItemAsActiveItem() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + Assert.AreEqual(screen.Object, this.conductor.ActiveItem); + } + + [Test] + public void InitialActivateDoesNotActivateItemIfConductorIsNotActive() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void InitialActivateActivatesItemIfConductorIsActive() + { + ((IActivate)this.conductor).Activate(); + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate()); + } + + [Test] + public void ActivatesActiveItemWhenActivated() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + + ((IActivate)this.conductor).Activate(); + screen.Verify(x => x.Activate()); + } + + [Test] + public void DeactivatesActiveItemWhenDeactivated() + { + ((IActivate)this.conductor).Activate(); + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + ((IDeactivate)this.conductor).Deactivate(); + screen.Verify(x => x.Deactivate()); + } + + [Test] + public void ActivateDeactivatesPreviousItemIfConductorIsActiveAndPreviousItemCanClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen1.Object); + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + this.conductor.ActivateItem(screen2.Object); + screen1.Verify(x => x.Deactivate()); + } + + [Test] + public void ActivatingCurrentScreenReactivatesScreen() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Exactly(2)); + screen.Verify(x => x.Close(), Times.Never); + } + + [Test] + public void CloseItemDoesNothingIfToldToDeactiveInactiveItem() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.CloseItem(screen2.Object); + + screen1.Verify(x => x.Close(), Times.Never); + screen2.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void DeactiveDoesNotChangeActiveItem() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + this.conductor.DeactivateItem(screen.Object); + + screen.Verify(x => x.Deactivate()); + Assert.AreEqual(this.conductor.ActiveItem, screen.Object); + } + + [Test] + public void ActivateSetsConductorAsItemsParent() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.VerifySet(x => x.Parent = this.conductor); + } + + [Test] + public void CloseRemovesItemsParent() + { + var screen = new Mock(); + screen.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen.Setup(x => x.Parent).Returns(this.conductor); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.VerifySet(x => x.Parent = null); + } + + [Test] + public void CanCloseReturnsTrueIfNoActiveItem() + { + Assert.IsTrue(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void CanCloseReturnsAllItemsCanClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.ActivateItem(screen2.Object); + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(false)); + Assert.IsFalse(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void DeactivatingActiveItemGoesBack() + { + ((IActivate)this.conductor).Activate(); + var screen1 = new Mock(); + var screen2 = new Mock(); + + this.conductor.ActivateItem(screen1.Object); + screen1.Verify(x => x.Activate()); + + this.conductor.ActivateItem(screen2.Object); + screen2.Verify(x => x.Activate()); + + this.conductor.DeactivateItem(screen2.Object); + screen2.Verify(x => x.Deactivate(), Times.Once); + screen1.Verify(x => x.Activate(), Times.Once); + } + + [Test] + public void ClearClosesAllItemsExceptCurrent() + { + ((IActivate)this.conductor).Activate(); + var screen1 = new Mock(); + var screen2 = new Mock(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.ActivateItem(screen2.Object); + this.conductor.Clear(); + + Assert.AreEqual(screen2.Object, this.conductor.ActiveItem); + + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + this.conductor.GoBack(); + Assert.IsNull(this.conductor.ActiveItem); + } + } +} diff --git a/StyletUnitTests/ConductorOneActiveTests.cs b/StyletUnitTests/ConductorOneActiveTests.cs new file mode 100644 index 0000000..c2daa5b --- /dev/null +++ b/StyletUnitTests/ConductorOneActiveTests.cs @@ -0,0 +1,246 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class ConductorOneActiveTests + { + private Conductor.Collections.OneActive conductor; + + [SetUp] + public void SetUp() + { + this.conductor = new Conductor.Collections.OneActive(); + } + + [Test] + public void NoActiveItemsBeforeAnyItemsActivated() + { + Assert.IsEmpty(this.conductor.Items); + } + + [Test] + public void ActivatingItemActivatesAndAddsToItems() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + + screen.Verify(x => x.Activate()); + Assert.AreEqual(screen.Object, this.conductor.ActiveItem); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void ActivatingItemDeactivatesPreviousItem() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + + this.conductor.ActivateItem(screen1.Object); + this.conductor.ActivateItem(screen2.Object); + + screen1.Verify(x => x.Deactivate()); + screen2.Verify(x => x.Activate()); + + Assert.AreEqual(screen2.Object, this.conductor.ActiveItem); + Assert.AreEqual(new[] { screen1.Object, screen2.Object }, this.conductor.Items); + } + + [Test] + public void ClosingActiveItemChoosesPreviousItemIfAvailable() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + var screen3 = new Mock(); + ((IActivate)this.conductor).Activate(); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object, screen3.Object }); + this.conductor.ActivateItem(screen2.Object); + + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + + this.conductor.CloseItem(screen2.Object); + Assert.AreEqual(screen1.Object, this.conductor.ActiveItem); + Assert.AreEqual(new[] { screen1.Object, screen3.Object }, this.conductor.Items); + } + + [Test] + public void ClosingActiveItemChoosesNextItemIfNoPreviousItem() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + var screen3 = new Mock(); + ((IActivate)this.conductor).Activate(); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object, screen3.Object }); + this.conductor.ActivateItem(screen3.Object); + + screen3.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + + this.conductor.CloseItem(screen3.Object); + Assert.AreEqual(screen2.Object, this.conductor.ActiveItem); + Assert.AreEqual(new[] { screen1.Object, screen2.Object }, this.conductor.Items); + } + + [Test] + public void ActivateItemDoesNotActivateIfConductorIsNotActive() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void ActivateItemDoesActiveIfConductorIsActive() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate()); + } + + [Test] + public void DeactiveDeactivatesItems() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + ((IDeactivate)this.conductor).Deactivate(); + screen.Verify(x => x.Deactivate()); + } + + [Test] + public void ClosingClosesAllItems() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + ((IClose)this.conductor).Close(); + screen.Verify(x => x.Close()); + Assert.AreEqual(0, this.conductor.Items.Count); + } + + [Test] + public void ConductorCanCloseIfAllItemsCanClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object }); + Assert.IsTrue(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void ConductorCanNotCloseIfAnyItemCanNotClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen2.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(false)); + + this.conductor.Items.AddRange(new[] { screen1.Object, screen2.Object }); + Assert.IsFalse(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void AddingItemSetsParent() + { + var screen = new Mock(); + this.conductor.Items.Add(screen.Object); + screen.VerifySet(x => x.Parent = this.conductor); + } + + [Test] + public void AddingItemDoesNotChangeActiveItem() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.Items.Add(screen2.Object); + + Assert.AreEqual(this.conductor.ActiveItem, screen1.Object); + screen2.Verify(x => x.Activate(), Times.Never); + screen1.Verify(x => x.Deactivate(), Times.Never); + } + + [Test] + public void RemovingItemClosesAndRemovesParent() + { + var screen = new Mock(); + screen.SetupGet(x => x.Parent).Returns(this.conductor); + this.conductor.Items.Add(screen.Object); + this.conductor.Items.Remove(screen.Object); + screen.VerifySet(x => x.Parent = null); + screen.Verify(x => x.Close()); + } + + [Test] + public void RemovingActiveItemActivatesAnotherItem() + { + ((IActivate)this.conductor).Activate(); + var screen1 = new Mock(); + var screen2 = new Mock(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.Items.Add(screen2.Object); + + this.conductor.Items.Remove(screen1.Object); + + Assert.AreEqual(this.conductor.ActiveItem, screen2.Object); + screen2.Verify(x => x.Activate()); + screen1.Verify(x => x.Close()); + } + + [Test] + public void AddingItemTwiceDoesNotResultInDuplicates() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.ActivateItem(screen.Object); + Assert.AreEqual(1, this.conductor.Items.Count); + } + + [Test] + public void DeactivateItemDeactivatesItemButDoesNotRemoveFromItems() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.DeactivateItem(screen.Object); + screen.Verify(x => x.Deactivate()); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void CloseItemDoesNothingIfItemCanNotClose() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.Verify(x => x.Close(), Times.Never); + Assert.That(this.conductor.Items, Is.EquivalentTo(new[] { screen.Object })); + } + + [Test] + public void CloseItemDeactivatesItemAndRemovesFromItemsIfItemCanClose() + { + var screen = new Mock(); + screen.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.Verify(x => x.Close()); + Assert.AreEqual(0, this.conductor.Items.Count); + } + } +} diff --git a/StyletUnitTests/ConductorTests.cs b/StyletUnitTests/ConductorTests.cs new file mode 100644 index 0000000..21e988f --- /dev/null +++ b/StyletUnitTests/ConductorTests.cs @@ -0,0 +1,172 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class ConductorTests + { + private Conductor conductor; + + [SetUp] + public void SetUp() + { + this.conductor = new Conductor(); + } + + [Test] + public void ActiveItemIsNullBeforeAnyItemsActivated() + { + Assert.IsNull(this.conductor.ActiveItem); + Assert.That(this.conductor.GetChildren(), Is.EquivalentTo(new IScreen[] { null })); + } + + [Test] + public void InitialActivateSetsItemAsActiveItem() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + Assert.AreEqual(screen.Object, this.conductor.ActiveItem); + } + + [Test] + public void InitialActivateDoesNotActivateItemIfConductorIsNotActive() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void InitialActivateActivatesItemIfConductorIsActive() + { + ((IActivate)this.conductor).Activate(); + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate()); + } + + [Test] + public void ActivatesActiveItemWhenActivated() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Never); + + ((IActivate)this.conductor).Activate(); + screen.Verify(x => x.Activate()); + } + + [Test] + public void DeactivatesActiveItemWhenDeactivated() + { + ((IActivate)this.conductor).Activate(); + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + ((IDeactivate)this.conductor).Deactivate(); + screen.Verify(x => x.Deactivate()); + } + + [Test] + public void ActivateClosesPreviousItemIfConductorIsActiveAndPreviousItemCanClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen1.Object); + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + this.conductor.ActivateItem(screen2.Object); + screen1.Verify(x => x.Close()); + } + + [Test] + public void ActivateDoesNothingIfPreviousItemCanNotClose() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen1.Object); + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(false)); + this.conductor.ActivateItem(screen2.Object); + + screen1.Verify(x => x.Close(), Times.Never); + screen2.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void ActivatingCurrentScreenReactivatesScreen() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + this.conductor.ActivateItem(screen.Object); + screen.Verify(x => x.Activate(), Times.Exactly(2)); + screen.Verify(x => x.Close(), Times.Never); + } + + [Test] + public void CloseItemDoesNothingIfToldToDeactiveInactiveItem() + { + var screen1 = new Mock(); + var screen2 = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen1.Object); + this.conductor.CloseItem(screen2.Object); + + screen1.Verify(x => x.Close(), Times.Never); + screen2.Verify(x => x.Activate(), Times.Never); + } + + [Test] + public void DeactiveDoesNotChangeActiveItem() + { + var screen = new Mock(); + ((IActivate)this.conductor).Activate(); + this.conductor.ActivateItem(screen.Object); + this.conductor.DeactivateItem(screen.Object); + + screen.Verify(x => x.Deactivate()); + Assert.AreEqual(this.conductor.ActiveItem, screen.Object); + } + + [Test] + public void ActivateSetsConductorAsItemsParent() + { + var screen = new Mock(); + this.conductor.ActivateItem(screen.Object); + screen.VerifySet(x => x.Parent = this.conductor); + } + + [Test] + public void CloseRemovesItemsParent() + { + var screen = new Mock(); + screen.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(true)); + screen.Setup(x => x.Parent).Returns(this.conductor); + this.conductor.ActivateItem(screen.Object); + this.conductor.CloseItem(screen.Object); + screen.VerifySet(x => x.Parent = null); + } + + [Test] + public void CanCloseReturnsTrueIfNoActiveItem() + { + Assert.IsTrue(this.conductor.CanCloseAsync().Result); + } + + [Test] + public void CanCloseReturnsActiveItemsCanClose() + { + var screen1 = new Mock(); + this.conductor.ActivateItem(screen1.Object); + screen1.Setup(x => x.CanCloseAsync()).Returns(Task.FromResult(false)); + Assert.IsFalse(this.conductor.CanCloseAsync().Result); + } + } +} diff --git a/StyletUnitTests/EventAggregatorTests.cs b/StyletUnitTests/EventAggregatorTests.cs new file mode 100644 index 0000000..d76b721 --- /dev/null +++ b/StyletUnitTests/EventAggregatorTests.cs @@ -0,0 +1,93 @@ +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class EventAggregatorTests + { + public class M1 { } + public class M2 : M1 { } + + public class C1 : IHandle + { + public M1 ReceivedMessage; + public void Handle(M1 message) { this.ReceivedMessage = message; } + } + + public class C2 : IHandle, IHandle + { + public M2 ReceivedM2; + public M1 ReceivedM1; + public void Handle(M2 message) { this.ReceivedM2 = message; } + public void Handle(M1 message) { this.ReceivedM1 = message; } + } + + public class C3 : IHandle + { + public void Handle(M1 message) { throw new Exception("Should not be called. Ever"); } + } + + [Test] + public void SubscribesAndDeliversExactMessage() + { + var ea = new EventAggregator(); + var target = new C1(); + ea.Subscribe(target); + + var message = new M1(); + ea.Publish(message); + + Assert.AreEqual(message, target.ReceivedMessage); + } + + [Test] + public void DeliversToAllHandlersIncludingDerived() + { + var ea = new EventAggregator(); + var target = new C2(); + ea.Subscribe(target); + + var message = new M2(); + ea.Publish(message); + + Assert.AreEqual(message, target.ReceivedM1); + Assert.AreEqual(message, target.ReceivedM2); + } + + [Test] + public void UnsubscribeUnsubscribes() + { + var ea = new EventAggregator(); + var target = new C1(); + ea.Subscribe(target); + ea.Unsubscribe(target); + + var message = new M1(); + ea.Publish(message); + + Assert.IsNull(target.ReceivedMessage); + } + + [Test] + public void TargetReferenceIsWeak() + { + var ea = new EventAggregator(); + var target = new C3(); + var weaktarget = new WeakReference(target); + ea.Subscribe(target); + + // Ugly, but it's the only way to test a WeakReference... + target = null; + GC.Collect(); + + Assert.DoesNotThrow(() => ea.Publish(new M1())); + Assert.IsNull(weaktarget.Target); + } + } +} diff --git a/StyletUnitTests/ExecuteTests.cs b/StyletUnitTests/ExecuteTests.cs new file mode 100644 index 0000000..381032c --- /dev/null +++ b/StyletUnitTests/ExecuteTests.cs @@ -0,0 +1,87 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class ExecuteTests + { + [Test] + public void OnUIThreadExecutesSynchronouslyIfNoSynchronizationContext() + { + Execute.SynchronizationContext = null; + bool blockWasRun = false; + Execute.OnUIThread(() => blockWasRun = true); + Assert.IsTrue(blockWasRun); + } + + [Test] + public void OnUIThreadExecutesAsynchronouslyIfSynchronizationContextIsNotNull() + { + var sync = new Mock(); + Execute.SynchronizationContext = sync.Object; + + SendOrPostCallback passedAction = null; + sync.Setup(x => x.Post(It.IsAny(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); + + bool actionCalled = false; + Execute.OnUIThread(() => actionCalled = true); + + Assert.IsFalse(actionCalled); + passedAction(null); + Assert.IsTrue(actionCalled); + } + + [Test] + public async Task OnUIThreadAsyncExecutesSynchronouslyIfNoSynchronizationContext() + { + Execute.SynchronizationContext = null; + bool blockWasRun = false; + await Execute.OnUIThreadAsync(() => blockWasRun = true); + Assert.IsTrue(blockWasRun); + } + + [Test] + public void OnUIThreadAsyncExecutesAsynchronouslyIfSynchronizationContextIsNotNull() + { + var sync = new Mock(); + Execute.SynchronizationContext = sync.Object; + + SendOrPostCallback passedAction = null; + sync.Setup(x => x.Post(It.IsAny(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); + + bool actionCalled = false; + var task = Execute.OnUIThreadAsync(() => actionCalled = true); + + Assert.IsFalse(task.IsCompleted); + Assert.IsFalse(actionCalled); + passedAction(null); + Assert.IsTrue(actionCalled); + Assert.IsTrue(task.IsCompleted); + } + + [Test] + public void OnUIThreadAsyncPropagatesException() + { + var sync = new Mock(); + Execute.SynchronizationContext = sync.Object; + + SendOrPostCallback passedAction = null; + sync.Setup(x => x.Post(It.IsAny(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); + + var ex = new Exception("test"); + var task = Execute.OnUIThreadAsync(() => { throw ex; }); + + passedAction(null); + Assert.IsTrue(task.IsFaulted); + Assert.AreEqual(ex, task.Exception.InnerExceptions[0]); + } + } +} diff --git a/StyletUnitTests/Properties/AssemblyInfo.cs b/StyletUnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..64387ab --- /dev/null +++ b/StyletUnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 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("StyletUnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Antony Male")] +[assembly: AssemblyProduct("StyletUnitTests")] +[assembly: AssemblyCopyright("Copyright © Antony Male 2014")] +[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)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ffe39040-7d51-464f-9f06-9df7f0284536")] + +// 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/StyletUnitTests/PropertyChangedBaseTests.cs b/StyletUnitTests/PropertyChangedBaseTests.cs new file mode 100644 index 0000000..c6ad117 --- /dev/null +++ b/StyletUnitTests/PropertyChangedBaseTests.cs @@ -0,0 +1,129 @@ +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class PropertyChangedBaseTests + { + class PropertyChanged : PropertyChangedBase + { + public int IntProperty { get; set; } + public string StringProperty + { + set { this.NotifyOfPropertyChange(); } + } + private double _doubleProperty; + public double DoubleProperty + { + get { return this._doubleProperty; } + set { SetAndNotify(ref this._doubleProperty, value); } + } + public void RaiseIntPropertyChangedWithExpression() + { + this.NotifyOfPropertyChange(() => this.IntProperty); + } + public void RaiseIntPropertyChangedWithString() + { + this.NotifyOfPropertyChange("IntProperty"); + } + } + + [Test] + public void RefreshRaisesPropertyChangedWithEmptyString() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + pc.Refresh(); + Assert.AreEqual(String.Empty, changedProperty); + } + + [Test] + public void NotifyOfPropertyChangedWithExpressionRaises() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + pc.RaiseIntPropertyChangedWithExpression(); + Assert.AreEqual("IntProperty", changedProperty); + } + + [Test] + public void NotifyOfPropertyChangedWithStringRaises() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + pc.RaiseIntPropertyChangedWithString(); + Assert.AreEqual("IntProperty", changedProperty); + } + + [Test] + public void NotifyOfPropertyChangedWithCallerMemberName() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + pc.StringProperty = "hello"; + Assert.AreEqual("StringProperty", changedProperty); + } + + [Test] + public void UsesDispatcher() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + + Action action = null; + pc.PropertyChangedDispatcher = a => action = a; + + pc.RaiseIntPropertyChangedWithExpression(); + Assert.IsNull(changedProperty); + Assert.IsNotNull(action); + + action(); + Assert.AreEqual("IntProperty", changedProperty); + } + + [Test] + public void UsesExecutesDispatcherByDefault() + { + Action action = null; + var oldDispatcher = Execute.DefaultPropertyChangedDispatcher; + Execute.DefaultPropertyChangedDispatcher = a => action = a; + + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + + pc.RaiseIntPropertyChangedWithExpression(); + Assert.IsNull(changedProperty); + Assert.IsNotNull(action); + + action(); + Assert.AreEqual("IntProperty", changedProperty); + + Execute.DefaultPropertyChangedDispatcher = oldDispatcher; + } + + [Test] + public void SetAndNotifyWorks() + { + var pc = new PropertyChanged(); + string changedProperty = null; + pc.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + + pc.DoubleProperty = 5; + + Assert.AreEqual("DoubleProperty", changedProperty); + Assert.AreEqual(5, pc.DoubleProperty); + } + } +} diff --git a/StyletUnitTests/PropertyChangedExtensionsTests.cs b/StyletUnitTests/PropertyChangedExtensionsTests.cs new file mode 100644 index 0000000..a0f629f --- /dev/null +++ b/StyletUnitTests/PropertyChangedExtensionsTests.cs @@ -0,0 +1,228 @@ +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class PropertyChangedExtensionsTests + { + class NotifyingClass : PropertyChangedBase + { + private string _foo; + public string Foo + { + get { return this._foo; } + set { SetAndNotify(ref this._foo, value); } + } + + private string _bar; + public string Bar + { + get { return this._bar; } + set { SetAndNotify(ref this._bar, value); } + } + + public void NotifyAll() + { + this.NotifyOfPropertyChange(String.Empty); + } + } + + class BindingClass + { + public string LastFoo; + private WeakEventManager weakEventManager = new WeakEventManager(); + + public IPropertyChangedBinding BindStrong(NotifyingClass notifying) + { + // Must make sure the compiler doesn't generate an inner class for this, otherwise we're not testing the right thing + return notifying.Bind(x => x.Foo, x => this.LastFoo = x); + } + + public IPropertyChangedBinding BindWeak(NotifyingClass notifying) + { + return this.weakEventManager.BindWeak(notifying, x => x.Foo, x => this.LastFoo = x); + } + } + + [Test] + public void StrongBindingBinds() + { + string newVal = null; + var c1 = new NotifyingClass(); + c1.Bind(x => x.Foo, x => newVal = x); + c1.Foo = "bar"; + + Assert.AreEqual("bar", newVal); + } + + [Test] + public void StrongBindingIgnoresOtherProperties() + { + string newVal = null; + var c1 = new NotifyingClass(); + c1.Bind(x => x.Bar, x => newVal = x); + c1.Foo = "bar"; + + Assert.AreEqual(null, newVal); + } + + [Test] + public void StrongBindingListensToEmptyString() + { + string newVal = null; + var c1 = new NotifyingClass(); + c1.Bar = "bar"; + c1.Bind(x => x.Bar, x => newVal = x); + c1.NotifyAll(); + + Assert.AreEqual("bar", newVal); + } + + [Test] + public void StrongBindingRetainsBindingClass() + { + var binding = new BindingClass(); + + // Means of determining whether the class has been disposed + var weakBinding = new WeakReference(binding); + + var notifying = new NotifyingClass(); + binding.BindStrong(notifying); + + binding = null; + GC.Collect(); + Assert.IsTrue(weakBinding.TryGetTarget(out binding)); + } + + [Test] + public void StrongBindingDoesNotRetainNotifier() + { + var binding = new BindingClass(); + var notifying = new NotifyingClass(); + // Means of determining whether the class has been disposed + var weakNotifying = new WeakReference(notifying); + // Retain the IPropertyChangedBinding, in case that causes NotifyingClass to be retained + var binder = binding.BindStrong(notifying); + + notifying = null; + GC.Collect(); + Assert.IsFalse(weakNotifying.TryGetTarget(out notifying)); + } + + [Test] + public void StrongBindingUnbinds() + { + string newVal = null; + var c1 = new NotifyingClass(); + var binding = c1.Bind(x => x.Bar, x => newVal = x); + binding.Unbind(); + c1.Bar = "bar"; + + Assert.AreEqual(null, newVal); + } + + [Test] + public void WeakBindingBinds() + { + var manager = new WeakEventManager(); + string newVal = null; + var c1 = new NotifyingClass(); + manager.BindWeak(c1, x => x.Foo, x => newVal = x); + c1.Foo = "bar"; + + Assert.AreEqual("bar", newVal); + } + + [Test] + public void WeakBindingIgnoresOtherProperties() + { + var manager = new WeakEventManager(); + string newVal = null; + var c1 = new NotifyingClass(); + manager.BindWeak(c1, x => x.Bar, x => newVal = x); + c1.Foo = "bar"; + + Assert.AreEqual(null, newVal); + } + + [Test] + public void WeakBindingListensToEmptyString() + { + var manager = new WeakEventManager(); + string newVal = null; + var c1 = new NotifyingClass(); + c1.Bar = "bar"; + manager.BindWeak(c1, x => x.Bar, x => newVal = x); + c1.NotifyAll(); + + Assert.AreEqual("bar", newVal); + } + + [Test] + public void WeakBindingDoesNotRetainBindingClass() + { + var binding = new BindingClass(); + + // Means of determining whether the class has been disposed + var weakBinding = new WeakReference(binding); + + var notifying = new NotifyingClass(); + binding.BindWeak(notifying); + + binding = null; + GC.Collect(); + Assert.IsFalse(weakBinding.TryGetTarget(out binding)); + } + + [Test] + public void WeakBindingRetainsClassIfIPropertyChangedBindingRetained() + { + var binding = new BindingClass(); + + // Means of determining whether the class has been disposed + var weakBinding = new WeakReference(binding); + + var notifying = new NotifyingClass(); + // Retain this + var binder = binding.BindWeak(notifying); + + binding = null; + GC.Collect(); + Assert.IsTrue(weakBinding.TryGetTarget(out binding)); + } + + [Test] + public void WeakBindingDoesNotRetainNotifier() + { + var binding = new BindingClass(); + var notifying = new NotifyingClass(); + // Means of determining whether the class has been disposed + var weakNotifying = new WeakReference(notifying); + // Retain binder, in case that affects anything + var binder = binding.BindWeak(notifying); + + notifying = null; + GC.Collect(); + Assert.IsFalse(weakNotifying.TryGetTarget(out notifying)); + } + + [Test] + public void WeakBindingUnbinds() + { + var manager = new WeakEventManager(); + string newVal = null; + var c1 = new NotifyingClass(); + var binding = manager.BindWeak(c1, x => x.Bar, x => newVal = x); + binding.Unbind(); + c1.Bar = "bar"; + + Assert.AreEqual(null, newVal); + } + } +} diff --git a/StyletUnitTests/ScreenTests.cs b/StyletUnitTests/ScreenTests.cs new file mode 100644 index 0000000..75ebf14 --- /dev/null +++ b/StyletUnitTests/ScreenTests.cs @@ -0,0 +1,237 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace StyletUnitTests +{ + [TestFixture] + public class ScreenTests + { + private class MyScreen : Screen + { + public bool OnActivateCalled; + protected override void OnActivate() + { + this.OnActivateCalled = true; + } + + public bool OnInitialActivateCalled; + protected override void OnInitialActivate() + { + this.OnInitialActivateCalled = true; + } + + public bool OnDeactivateCalled; + protected override void OnDeactivate() + { + this.OnDeactivateCalled = true; + } + + public bool OnCloseCalled; + protected override void OnClose() + { + this.OnCloseCalled = true; + } + + public bool OnViewLoadedCalled; + protected override void OnViewLoaded() + { + this.OnViewLoadedCalled = true; + } + } + + private MyScreen screen; + + [SetUp] + public void SetUp() + { + this.screen = new MyScreen(); + } + + [Test] + public void SettingDisplayNameNotifies() + { + string changedProperty = null; + this.screen.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + + this.screen.DisplayName = "test"; + + Assert.AreEqual("test", this.screen.DisplayName); + Assert.AreEqual("DisplayName", changedProperty); + } + + [Test] + public void ScreenIsInitiallyNotActive() + { + Assert.IsFalse(this.screen.IsActive); + } + + [Test] + public void ActivateActivatesIfNotAlreadyActive() + { + ((IActivate)this.screen).Activate(); + Assert.IsTrue(this.screen.IsActive); + } + + [Test] + public void ActivateFiresActivatedEvent() + { + bool fired = false; + this.screen.Activated += (o, e) => fired = true; + ((IActivate)this.screen).Activate(); + Assert.IsTrue(fired); + } + + [Test] + public void ActivateCallsOnActivate() + { + ((IActivate)this.screen).Activate(); + Assert.IsTrue(this.screen.OnActivateCalled); + } + + [Test] + public void DoubleActivationDoesntActivate() + { + ((IActivate)this.screen).Activate(); + this.screen.OnActivateCalled = false; + ((IActivate)this.screen).Activate(); + Assert.IsFalse(this.screen.OnActivateCalled); + } + + [Test] + public void InitialActivationCallsOnInitialActivate() + { + ((IActivate)this.screen).Activate(); + this.screen.OnInitialActivateCalled = false; + ((IDeactivate)this.screen).Deactivate(); + ((IActivate)this.screen).Activate(); + Assert.IsFalse(this.screen.OnInitialActivateCalled); + } + + [Test] + public void DeactivateDeactivates() + { + ((IActivate)this.screen).Activate(); ; + ((IDeactivate)this.screen).Deactivate(); + Assert.IsFalse(this.screen.IsActive); + } + + [Test] + public void DeactivateFiredDeactivatedEvent() + { + bool fired = false; + this.screen.Deactivated += (o, e) => fired = true; + ((IActivate)this.screen).Activate(); ; + ((IDeactivate)this.screen).Deactivate(); + Assert.IsTrue(fired); + } + + [Test] + public void DeactivateCallsOnDeactivate() + { + ((IActivate)this.screen).Activate(); + ((IDeactivate)this.screen).Deactivate(); + Assert.IsTrue(this.screen.OnDeactivateCalled); + } + + [Test] + public void DoubleDeactivationDoesntDeactivate() + { + ((IActivate)this.screen).Activate(); + ((IDeactivate)this.screen).Deactivate(); + this.screen.OnDeactivateCalled = false; + ((IDeactivate)this.screen).Deactivate(); + Assert.IsFalse(this.screen.OnDeactivateCalled); + } + + [Test] + public void CloseDeactivates() + { + ((IActivate)this.screen).Activate(); + ((IClose)this.screen).Close(); + Assert.IsTrue(this.screen.OnDeactivateCalled); + } + + [Test] + public void CloseClearsView() + { + ((IViewAware)this.screen).AttachView(new UIElement()); + ((IClose)this.screen).Close(); + Assert.IsNull(this.screen.View); + } + + [Test] + public void CloseFiresClosed() + { + bool fired = false; + this.screen.Closed += (o, e) => fired = true; + ((IClose)this.screen).Close(); + Assert.IsTrue(fired); + } + + [Test] + public void CloseCallsOnClose() + { + ((IClose)this.screen).Close(); + Assert.IsTrue(this.screen.OnCloseCalled); + } + + [Test] + public void AttachViewAttachesView() + { + var view = new UIElement(); + ((IViewAware)this.screen).AttachView(view); + Assert.AreEqual(view, this.screen.View); + } + + [Test] + public void AttachViewThrowsIfViewAlreadyAttached() + { + var view = new UIElement(); + ((IViewAware)this.screen).AttachView(view); + Assert.Throws(() => ((IViewAware)this.screen).AttachView(view)); + } + + [Test] + public void SettingParentRaisesPropertyChange() + { + var parent = new object(); + string changedProperty = null; + this.screen.PropertyChanged += (o, e) => changedProperty = e.PropertyName; + this.screen.Parent = parent; + + Assert.AreEqual(parent, this.screen.Parent); + Assert.AreEqual("Parent", changedProperty); + } + + [Test] + public void CanCloseAsyncReturnsCompletedTrueTask() + { + var task = this.screen.CanCloseAsync(); + Assert.IsTrue(task.IsCompleted); + Assert.IsTrue(task.Result); + } + + [Test] + public void TryCloseThrowsIfParentIsNotIChildDelegate() + { + this.screen.Parent = new object(); + Assert.Throws(() => this.screen.TryClose()); + } + + [Test] + public void TryCloseCallsParentCloseItemPassingDialogResult() + { + var parent = new Mock(); + screen.Parent = parent.Object; + this.screen.TryClose(true); + parent.Verify(x => x.CloseItem(this.screen, true)); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCAutobindingTests.cs b/StyletUnitTests/StyletIoC/StyletIoCAutobindingTests.cs new file mode 100644 index 0000000..63069e3 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCAutobindingTests.cs @@ -0,0 +1,121 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCAutobindingTests + { + interface I1 { } + class C11 : I1 { } + class C12 : I1 { } + abstract class C13 : I1 { } + + interface I2 { } + class C21 : I2 { } + class C22 : I2 { } + + [Inject("Key")] + class C3 { } + + [Test] + public void NongenericInterfaceToAllImplementations() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAllImplementations(); + var ioc = builder.BuildContainer(); + + var result = ioc.GetAll().ToList(); + Assert.AreEqual(2, result.Count); + Assert.IsInstanceOf(result[0]); + Assert.IsInstanceOf(result[1]); + } + + [Test] + public void GenericInterfaceToAllImplementations() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(I2<>)).ToAllImplementations(); + var ioc = builder.BuildContainer(); + + var result = ioc.GetAll>().ToList(); + Assert.AreEqual(2, result.Count); + Assert.IsInstanceOf>(result[0]); + Assert.IsInstanceOf>(result[1]); + } + + [Test] + public void IgnoresAllImplementsWhichIsNotPossible() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAllImplementations(); + var ioc = builder.BuildContainer(); + + var result = ioc.GetAll().ToList(); + Assert.AreEqual(2, result.Count); + Assert.IsNotInstanceOf(result[0]); + Assert.IsNotInstanceOf(result[1]); + } + + [Test] + public void AutobindingBindsConcreteTypes() + { + var builder = new StyletIoCBuilder(); + builder.Autobind(); + var ioc = builder.BuildContainer(); + + var result = ioc.Get(); + Assert.IsInstanceOf(result); + } + + [Test] + public void AutobindingBindsGenericTypes() + { + var builder = new StyletIoCBuilder(); + builder.Autobind(); + var ioc = builder.BuildContainer(); + + var result = ioc.Get>(); + Assert.IsInstanceOf>(result); + } + + [Test] + public void AutobindingDoesNotBindInterfaceTypes() + { + var builder = new StyletIoCBuilder(); + builder.Autobind(); + var ioc = builder.BuildContainer(); + + Assert.Throws(() => ioc.Get()); + } + + [Test] + public void AutobindingRespectsKeys() + { + var builder = new StyletIoCBuilder(); + builder.Autobind(); + var ioc = builder.BuildContainer(); + + var result = ioc.Get("Key"); + Assert.IsInstanceOf(result); + } + + [Test] + public void AutobindingBindingsCanBeReplaced() + { + var builder = new StyletIoCBuilder(); + builder.Autobind(); + builder.Bind().ToSelf().InSingletonScope(); + var ioc = builder.BuildContainer(); + + var result1 = ioc.Get(); + var result2 = ioc.Get(); + Assert.AreEqual(result2, result1); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCBindingChecksTests.cs b/StyletUnitTests/StyletIoC/StyletIoCBindingChecksTests.cs new file mode 100644 index 0000000..fa001b4 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCBindingChecksTests.cs @@ -0,0 +1,69 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCBindingChecksTests + { + interface I1 { } + class C2 { } + interface I3 : I1 { } + abstract class C4 : I1 { } + class C5 : I1 { } + + interface I6 { } + class C6 : I6 { } + interface I7 { } + class C7 { } + + [Test] + public void ThrowsIfTypeDoesNotImplementService() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() =>builder.Bind().To()); + } + + [Test] + public void ThrowsIfImplementationIsNotConcrete() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() => builder.Bind().To()); + Assert.Throws(() => builder.Bind().To()); + } + + [Test] + public void ThrowsIfImplementationIsSingletonUnboundGeneric() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() => builder.Bind().To(typeof(C5<>)).InSingletonScope()); + } + + [Test] + public void ThrowsIfUnboundGenericServiceBoundToNormalImplementation() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() => builder.Bind(typeof(I6<>)).To>()); + } + + [Test] + public void ThrowsIfNormalServiceBoundToUnboundGenericService() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() => builder.Bind>().To(typeof(C6<>))); + } + + [Test] + public void ThrowsIfUnboundTypesHaveDifferentNumbersOfTypeParameters() + { + var builder = new StyletIoCBuilder(); + Assert.Throws(() => builder.Bind(typeof(I6<>)).To(typeof(C7<,>))); + Assert.Throws(() => builder.Bind(typeof(I7<,>)).To(typeof(C6<>))); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCConstructorInjectionTests.cs b/StyletUnitTests/StyletIoC/StyletIoCConstructorInjectionTests.cs new file mode 100644 index 0000000..c9ad9f5 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCConstructorInjectionTests.cs @@ -0,0 +1,219 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + interface I1 { } + + class C1 : I1 { } + class C2 : I1 + { + public C1 C1; + public C2(C1 c1) + { + this.C1 = c1; + } + } + + class C3 + { + public C1 C1; + public C2 C2; + public C3(C1 c1, C2 c2) + { + this.C1 = c1; + this.C2 = c2; + } + } + + class C4 + { + public C1 C1; + public C4([Inject("key1")] C1 c1) + { + this.C1 = c1; + } + } + + class C5 + { + public bool RightConstructorCalled; + public C5(C1 c1, C2 c2 = null, C3 c3 = null, C4 c4 = null) + { + } + + public C5(C1 c1, C2 c2, C3 c3 = null) + { + this.RightConstructorCalled = true; + } + + public C5(C1 c1, C2 c2) + { + } + } + + class C6 + { + public bool RightConstructorCalled; + [Inject] + public C6(C1 c1) + { + this.RightConstructorCalled = true; + } + + public C6(C1 c1, C2 c2) + { + } + } + + class C7 + { + [Inject] + public C7() + { + } + + [Inject] + public C7(C1 c1) + { + } + } + + class C8 + { + public IEnumerable I1s; + public C8(IEnumerable i1s) + { + this.I1s = i1s; + } + } + + [TestFixture] + public class StyletIoCConstructorInjectionTests + { + [Test] + public void RecursivelyPopulatesConstructorParams() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c3 = ioc.Get(); + + Assert.IsInstanceOf(c3); + Assert.IsInstanceOf(c3.C1); + Assert.IsInstanceOf(c3.C2); + Assert.IsInstanceOf(c3.C2.C1); + } + + [Test] + public void UsesConstructorParamKeys() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf().WithKey("key1"); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c4 = ioc.Get(); + + Assert.IsInstanceOf(c4.C1); + } + + [Test] + public void ThrowsIfConstructorParamKeyNotRegistered() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + Assert.Throws(() => ioc.Get()); + } + + [Test] + public void ChoosesCtorWithMostParamsWeCanFulfill() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c5 = ioc.Get(); + Assert.IsTrue(c5.RightConstructorCalled); + } + + [Test] + public void ChoosesCtorWithAttribute() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c6 = ioc.Get(); + Assert.IsTrue(c6.RightConstructorCalled); + } + + [Test] + public void ThrowsIfMoreThanOneCtorWithAttribute() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + Assert.Throws(() => ioc.Get()); + } + + [Test] + public void ThrowsIfNoCtorAvailable() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + Assert.Throws(() => ioc.Get()); + } + + [Test] + public void SingletonActuallySingleton() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf().InSingletonScope(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c3 = ioc.Get(); + Assert.AreEqual(ioc.Get(), c3.C1); + Assert.AreEqual(ioc.Get().C1, c3.C1); + } + + [Test] + public void IEnumerableHasAllInjected() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().To(); + builder.Bind().To(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var c8 = ioc.Get(); + var i1s = c8.I1s.ToList(); + + Assert.AreEqual(2, i1s.Count); + Assert.IsInstanceOf(i1s[0]); + Assert.IsInstanceOf(i1s[1]); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCFactoryTests.cs b/StyletUnitTests/StyletIoC/StyletIoCFactoryTests.cs new file mode 100644 index 0000000..ac1fe3d --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCFactoryTests.cs @@ -0,0 +1,165 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCFactoryTests + { + public interface I1 { } + public class C1 : I1 { } + public class C12 : I1 { } + + public interface I1Factory + { + I1 GetI1(); + } + + public interface I1Factory2 + { + I1 GetI1(string key = null); + } + + public interface I1Factory3 + { + IEnumerable GetAllI1s(); + } + + public interface IFactoryWithKeys + { + [Inject("Key")] + I1 GetI1WithoutKey(); + + [Inject("Key")] + I1 GetI1WithKey(string key); + } + + interface IPrivateFactory + { + } + + public interface IFactoryWithBadMethod + { + C1 MethodWithArgs(bool arg); + } + + public interface IFactoryWithVoidMethod + { + void Method(); + } + + [Test] + public void CreatesImplementationWithoutKey() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().ToAbstractFactory(); + var ioc = builder.BuildContainer(); + + var factory = ioc.Get(); + Assert.IsNotNull(factory); + + var result = factory.GetI1(); + Assert.IsInstanceOf(result); + } + + [Test] + public void CreatesImplementationWithKey() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().WithKey("key"); + builder.Bind().ToAbstractFactory(); + var ioc = builder.BuildContainer(); + + var factory = ioc.Get(); + Assert.IsNotNull(factory); + + var result = factory.GetI1("key"); + Assert.IsInstanceOf(result); + } + + [Test] + public void CreatesAllImplementations() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().To(); + builder.Bind().ToAbstractFactory(); + var ioc = builder.BuildContainer(); + + var factory = ioc.Get(); + Assert.IsNotNull(factory); + + var results = factory.GetAllI1s().ToList(); + + Assert.AreEqual(2, results.Count); + Assert.IsInstanceOf(results[0]); + Assert.IsInstanceOf(results[1]); + } + + [Test] + public void UsesAttributeKeyIfKeyParameterNotGiven() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().WithKey("Key"); + builder.Bind().To(); + builder.Bind().ToAbstractFactory(); + var ioc = builder.BuildContainer(); + + var factory = ioc.Get(); + var result = factory.GetI1WithoutKey(); + Assert.IsInstanceOf(result); + } + + [Test] + public void UsesParameterKeyInPreferenceToAttributeKey() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().WithKey("Key2"); + builder.Bind().To(); + builder.Bind().ToAbstractFactory(); + var ioc = builder.BuildContainer(); + + var factory = ioc.Get(); + var result = factory.GetI1WithKey("Key2"); + Assert.IsInstanceOf(result); + } + + [Test] + public void ThrowsIfServiceTypeIsNotInterface() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAbstractFactory(); + Assert.Throws(() => builder.BuildContainer()); + } + + [Test] + public void ThrowsIfInterfaceNotPublic() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAbstractFactory(); + Assert.Throws(() => builder.BuildContainer()); + } + + [Test] + public void ThrowsIfMethodHasArgumentOtherThanString() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAbstractFactory(); + Assert.Throws(() => builder.BuildContainer()); + } + + [Test] + public void ThrowsIfMethodReturningVoid() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToAbstractFactory(); + Assert.Throws(() => builder.BuildContainer()); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCGetAllTests.cs b/StyletUnitTests/StyletIoC/StyletIoCGetAllTests.cs new file mode 100644 index 0000000..6290327 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCGetAllTests.cs @@ -0,0 +1,110 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCGetAllTests + { + interface IC1 { } + class C11 : IC1 { } + class C12 : IC1 { } + class C13 : IC1 { } + + interface IC2 { } + class C21 : IC2 { } + class C22 : IC2 { } + + // Tests that Bind() and friends worked was done in StyletIoCGetSingleTests + + [Test] + public void ImplementationTransientBindingsResolveGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().To(); + builder.Bind().To(); + builder.Bind().To(); + var ioc = builder.BuildContainer(); + + var results1 = ioc.GetAll().ToList(); + var results2 = ioc.GetAll().ToList(); + + Assert.AreEqual(results1.Count, 3); + + Assert.IsInstanceOf(results1[0]); + Assert.IsInstanceOf(results1[1]); + Assert.IsInstanceOf(results1[2]); + + Assert.That(results1, Is.Not.EquivalentTo(results2)); + } + + [Test] + public void ImplementationTransientBindingsResolveTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(IC1)).To(typeof(C11)); + builder.Bind(typeof(IC1)).To(typeof(C12)); + builder.Bind(typeof(IC1)).To(typeof(C13)); + builder.Bind(typeof(IC2)).To(typeof(C21)); + var ioc = builder.BuildContainer(); + + var results1 = ioc.GetAll(typeof(IC1)).ToList(); + var results2 = ioc.GetAll(typeof(IC1)).ToList(); + + Assert.AreEqual(results1.Count, 3); + Assert.IsInstanceOf(results1[0]); + Assert.IsInstanceOf(results1[1]); + Assert.IsInstanceOf(results1[2]); + + Assert.That(results1, Is.Not.EquivalentTo(results2)); + } + + [Test] + public void SingletonBindingsResolveGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().InSingletonScope(); + builder.Bind().To().InSingletonScope(); + builder.Bind().To().InSingletonScope(); + builder.Bind().To().InSingletonScope(); + var ioc = builder.BuildContainer(); + + var results1 = ioc.GetAll().ToList(); + var results2 = ioc.GetAll().ToList(); + + Assert.AreEqual(results1.Count, 3); + Assert.IsInstanceOf(results1[0]); + Assert.IsInstanceOf(results1[1]); + Assert.IsInstanceOf(results1[2]); + + Assert.That(results1, Is.EquivalentTo(results2)); + } + + [Test] + public void SingletonBindingsResolveTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(IC1)).To(typeof(C11)).InSingletonScope(); + builder.Bind(typeof(IC1)).To(typeof(C12)).InSingletonScope(); + builder.Bind(typeof(IC1)).To(typeof(C13)).InSingletonScope(); + builder.Bind(typeof(IC2)).To(typeof(C21)).InSingletonScope(); + var ioc = builder.BuildContainer(); + + var results1 = ioc.GetAll(typeof(IC1)).ToList(); + var results2 = ioc.GetAll(typeof(IC1)).ToList(); + + Assert.AreEqual(results1.Count, 3); + Assert.IsInstanceOf(results1[0]); + Assert.IsInstanceOf(results1[1]); + Assert.IsInstanceOf(results1[2]); + + Assert.That(results1, Is.EquivalentTo(results2)); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCGetSingleKeyedTests.cs b/StyletUnitTests/StyletIoC/StyletIoCGetSingleKeyedTests.cs new file mode 100644 index 0000000..2cefc23 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCGetSingleKeyedTests.cs @@ -0,0 +1,72 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCGetSingleKeyedTests + { + interface IC { } + class C1 : IC { } + class C2 : IC { } + class C3 : IC { } + + [Inject("key1")] + class C4 : IC { } + + [Test] + public void GetReturnsKeyedType() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().WithKey("key1"); + builder.Bind().To().WithKey("key2"); + var ioc = builder.BuildContainer(); + + Assert.IsInstanceOf(ioc.Get("key1")); + Assert.IsInstanceOf(ioc.Get("key2")); + } + + [Test] + public void GetAllReturnsKeyedTypes() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().WithKey("key1"); + builder.Bind().To().WithKey("key1"); + builder.Bind().To(); + var ioc = builder.BuildContainer(); + + var results = ioc.GetAll("key1").ToList(); + + Assert.AreEqual(results.Count, 2); + Assert.IsInstanceOf(results[0]); + Assert.IsInstanceOf(results[1]); + } + + [Test] + public void AttributeIsUsed() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().To(); + var ioc = builder.BuildContainer(); + + Assert.IsInstanceOf(ioc.Get("key1")); + } + + [Test] + public void GivenKeyOverridesAttribute() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + builder.Bind().To().WithKey("key2"); + var ioc = builder.BuildContainer(); + + Assert.IsInstanceOf(ioc.Get("key2")); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCGetSingleTests.cs b/StyletUnitTests/StyletIoC/StyletIoCGetSingleTests.cs new file mode 100644 index 0000000..5a3e28e --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCGetSingleTests.cs @@ -0,0 +1,185 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCGetSingleTests + { + interface IC1 { } + class C1 : IC1 { } + + [Test] + public void SelfTransientBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void SelfTransientBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(C1)).ToSelf(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(C1)); + var obj2 = ioc.Get(typeof(C1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void SelfSingletonBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf().InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + + [Test] + public void SelfSingletonBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(C1)).ToSelf().InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(C1)); + var obj2 = ioc.Get(typeof(C1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + + [Test] + public void FactoryTransientBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToFactory(c => new C1()); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void FactoryTransientBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(C1)).ToFactory(c => new C1()); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(C1)); + var obj2 = ioc.Get(typeof(C1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void FactorySingletonBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToFactory(c => new C1()).InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + + [Test] + public void FactorySingletonBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(C1)).ToFactory(c => new C1()).InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(C1)); + var obj2 = ioc.Get(typeof(C1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + + [Test] + public void ImplementationTransientBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void ImplementationTransientBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(IC1)).To(typeof(C1)); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(IC1)); + var obj2 = ioc.Get(typeof(IC1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.Not.EqualTo(obj2)); + } + + [Test] + public void ImplementationSingletonBindingResolvesGeneric() + { + var builder = new StyletIoCBuilder(); + builder.Bind().To().InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(); + var obj2 = ioc.Get(); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + + [Test] + public void ImplementationSingletonBindingResolvesTyped() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(IC1)).To(typeof(C1)).InSingletonScope(); + var ioc = builder.BuildContainer(); + + var obj1 = ioc.Get(typeof(IC1)); + var obj2 = ioc.Get(typeof(IC1)); + + Assert.That(obj1, Is.Not.Null); + Assert.That(obj1, Is.EqualTo(obj2)); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCPropertyInjectionTests.cs b/StyletUnitTests/StyletIoC/StyletIoCPropertyInjectionTests.cs new file mode 100644 index 0000000..b19ae84 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCPropertyInjectionTests.cs @@ -0,0 +1,192 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCPropertyInjectionTests + { + class C1 { } + interface I2 { } + class C2 : I2 { } + + class Subject1 + { + public C1 Ignored = null; + + [Inject] + public C1 C1 = null; + } + + class Subject2 + { + [Inject] + private C1 c1 = null; + public C1 GetC1() { return this.c1; } + } + + class Subject3 + { + [Inject] + public C1 C1 { get; set; } + } + + class Subject4 + { + [Inject] + public C1 C11 { get; private set; } + [Inject] + private C1 C12 { get; set; } + + public C1 GetC11() { return this.C11; } + public C1 GetC12() { return this.C12; } + } + + class Subject5 + { + [Inject("key")] + public C1 C1 = null; + } + + class Subject6 : IInjectionAware + { + [Inject] + public C1 C1 = null; + + public bool ParametersInjectedCalledCorrectly; + public void ParametersInjected() { this.ParametersInjectedCalledCorrectly = this.C1 != null; } + } + + class C3 + { + public C3(C4 c2) { } + } + class C4 + { + public C4(C3 c3) { } + } + + [Test] + public void BuildsUpPublicFields() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = new Subject1(); + ioc.BuildUp(subject); + + Assert.IsInstanceOf(subject.C1); + Assert.IsNull(subject.Ignored); + } + + [Test] + public void BuildsUpPrivateFields() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = new Subject2(); + ioc.BuildUp(subject); + + Assert.IsInstanceOf(subject.GetC1()); + } + + [Test] + public void BuildsUpPublicProperties() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = new Subject3(); + ioc.BuildUp(subject); + + Assert.IsInstanceOf(subject.C1); + } + + [Test] + public void BuildsUpPrivateProperties() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = new Subject4(); + ioc.BuildUp(subject); + + Assert.IsInstanceOf(subject.GetC11()); + Assert.IsInstanceOf(subject.GetC12()); + } + + [Test] + public void RespectsKeys() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf().WithKey("key"); + var ioc = builder.BuildContainer(); + + var subject = new Subject5(); + ioc.BuildUp(subject); + + Assert.IsInstanceOf(subject.C1); + } + + [Test] + public void ThrowsIfCanNotResolve() + { + var builder = new StyletIoCBuilder(); + var ioc = builder.BuildContainer(); + + var subject = new Subject1(); + Assert.Throws(() => ioc.BuildUp(subject)); + } + + [Test] + public void BuildsUpParametersOfNewlyCreatedType() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = ioc.Get(); + + Assert.IsInstanceOf(subject.C1); + Assert.IsNull(subject.Ignored); + } + + [Test] + public void CallsParametersInjectedAfterInjectingParameters() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToSelf(); + var ioc = builder.BuildContainer(); + + var subject = ioc.Get(); + + Assert.IsInstanceOf(subject.C1); + Assert.IsTrue(subject.ParametersInjectedCalledCorrectly); + } + + [Test] + public void FactoryCreatorBuildsUp() + { + var builder = new StyletIoCBuilder(); + builder.Bind().ToSelf(); + builder.Bind().ToFactory(c => new Subject1()); + var ioc = builder.BuildContainer(); + + var subject = ioc.Get(); + + Assert.IsInstanceOf(subject.C1); + } + } +} diff --git a/StyletUnitTests/StyletIoC/StyletIoCUnboundGenericTests.cs b/StyletUnitTests/StyletIoC/StyletIoCUnboundGenericTests.cs new file mode 100644 index 0000000..8400ca5 --- /dev/null +++ b/StyletUnitTests/StyletIoC/StyletIoCUnboundGenericTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using StyletIoC; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StyletUnitTests +{ + [TestFixture] + public class StyletIoCUnboundGenericTests + { + interface I1 { } + class C1 : I1 { } + + interface I2 { } + class C2 : I2 { } + + [Test] + public void ResolvesSingleGenericType() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(C1<>)).ToSelf(); + var ioc = builder.BuildContainer(); + + Assert.DoesNotThrow(() => ioc.Get>()); + } + + [Test] + public void ResolvesGenericTypeFromInterface() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(I1<>)).To(typeof(C1<>)); + var ioc = builder.BuildContainer(); + + var result = ioc.Get>(); + Assert.IsInstanceOf>(result); + } + + [Test] + public void ResolvesGenericTypeWhenOrderOfTypeParamsChanged() + { + var builder = new StyletIoCBuilder(); + builder.Bind(typeof(I2<,>)).To(typeof(C2<,>)); + var ioc = builder.BuildContainer(); + + var c2 = ioc.Get>(); + Assert.IsInstanceOf>(c2); + } + } +} diff --git a/StyletUnitTests/StyletUnitTests.csproj b/StyletUnitTests/StyletUnitTests.csproj new file mode 100644 index 0000000..9ab9e26 --- /dev/null +++ b/StyletUnitTests/StyletUnitTests.csproj @@ -0,0 +1,98 @@ + + + + + Debug + AnyCPU + {13AFA20D-CCEA-4A58-920E-4D8816C7296B} + Library + Properties + StyletUnitTests + StyletUnitTests + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\Sandbox\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll + + + ..\packages\NUnit.2.6.3\lib\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {2435bd00-ac12-48b0-ad36-9bab2fdec3f5} + Stylet + + + + + + + + + \ No newline at end of file diff --git a/StyletUnitTests/WindowManagerTests.cs b/StyletUnitTests/WindowManagerTests.cs new file mode 100644 index 0000000..1acf8d6 --- /dev/null +++ b/StyletUnitTests/WindowManagerTests.cs @@ -0,0 +1,47 @@ +using Moq; +using NUnit.Framework; +using Stylet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace StyletUnitTests +{ + [TestFixture, RequiresSTA] + public class WindowManagerTests + { + private Mock viewManager; + private WindowManager windowManager; + + [SetUp] + public void SetUp() + { + this.viewManager = new Mock(); + this.windowManager = new WindowManager(); + + IoC.GetInstance = (service, key) => this.viewManager.Object; + } + + [Test] + public void ShowWindowAsksViewManagerForView() + { + var model = new object(); + this.viewManager.Setup(x => x.CreateViewForModel(model)).Verifiable(); + // Don't care if this throws - that's OK + try { this.windowManager.ShowWindow(model); } + catch (Exception) { } + this.viewManager.VerifyAll(); + } + + [Test] + public void ShowWindowThrowsIfViewIsntAWindow() + { + var model = new object(); + this.viewManager.Setup(x => x.CreateViewForModel(model)).Returns(new UIElement()); + Assert.Throws(() => this.windowManager.ShowWindow(model)); + } + } +} diff --git a/StyletUnitTests/packages.config b/StyletUnitTests/packages.config new file mode 100644 index 0000000..5cd66f2 --- /dev/null +++ b/StyletUnitTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file