├── .gitignore ├── .vs └── config │ └── applicationhost.config ├── Demo ├── TestWPF │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ ├── Settings.Designer.cs │ │ └── Settings.settings │ ├── Services.cs │ ├── Settings │ │ ├── AppSettings.cs │ │ ├── DisplaySettings.cs │ │ └── GeneralSettings.cs │ ├── TestWPF.csproj │ ├── app.config │ └── packages.config └── TestWinForms │ ├── ColorPickerUC.Designer.cs │ ├── ColorPickerUC.cs │ ├── ColorPickerUC.resx │ ├── Person.cs │ ├── Program.cs │ ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings │ ├── Services.cs │ ├── TestForm.Designer.cs │ ├── TestForm.cs │ ├── TestForm.resx │ ├── TestWinForms.csproj │ ├── TestWinForms.csproj.user │ ├── app.config │ └── packages.config ├── Jot.Tests ├── Jot.Tests.csproj ├── JsonFileStoreTests.cs ├── TestData │ ├── Bar.cs │ ├── Foo.cs │ ├── Foo2.cs │ ├── FooAtt.cs │ ├── TestStore.cs │ └── TrackingAwareTestClass.cs └── TrackerTests.cs ├── Jot.sln ├── Jot ├── Configuration │ ├── Attributes │ │ ├── PersistTriggerAttributete.cs │ │ ├── StopTrackingTrigger.cs │ │ ├── TrackableAttribute.cs │ │ └── TrackingKeyAttribute.cs │ ├── ITrackingAware.cs │ ├── ITrackingConfiguration.cs │ ├── TrackedPropertyInfo.cs │ ├── TrackingConfiguration.cs │ ├── TrackingConfigurationGeneric.cs │ ├── TrackingOperationEventArgs.cs │ └── Trigger.cs ├── Jot.csproj ├── Jot.csproj.user ├── Jot.xml ├── Properties │ └── launchSettings.json ├── Storage │ ├── IStore.cs │ └── JsonFileStore.cs ├── Tracker.cs └── mykey.snk ├── LICENSE.txt ├── README.md └── _config.yml /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | bin/ 3 | Debug/ 4 | Release/ 5 | TestResults/ 6 | packages/ 7 | Ignored/ 8 | .vs/ 9 | 10 | storage.ide 11 | *.nupkg 12 | *.exe 13 | *.dll 14 | *.suo 15 | *.ncb 16 | *.vspcc 17 | *.vscc 18 | *.vssscc 19 | *.scc 20 | *.vspscc 21 | *.obproj.vspscc 22 | *.obproj.map 23 | *.cache 24 | 25 | -------------------------------------------------------------------------------- /Demo/TestWPF/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/TestWPF/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Windows; 7 | using TestWPF.Settings; 8 | 9 | namespace TestWPF 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | public static AppSettings Settings = new AppSettings(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Demo/TestWPF/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Demo/TestWPF/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Jot.Configuration; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using TestWPF.Settings; 6 | 7 | namespace TestWPF 8 | { 9 | /// 10 | /// Interaction logic for MainWindow.xaml 11 | /// 12 | public partial class MainWindow : Window 13 | { 14 | 15 | public MainWindow() 16 | { 17 | InitializeComponent(); 18 | 19 | //set up tracking and apply state to the application settings object 20 | Services.Tracker.Track(App.Settings); 21 | 22 | // in addition to tracking standard window properties, also track selected tab and first-grid-column width for MainWindow instances 23 | Services.Tracker.Configure() 24 | .Property(w => w.tabControl.SelectedIndex, "SelectedTab") 25 | .Property(w => w.firstCol.Width); 26 | 27 | //set up tracking and apply state for the main window 28 | Services.Tracker.Track(this); 29 | 30 | this.DataContext = App.Settings; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Demo/TestWPF/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("TestWPF")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("Microsoft")] 14 | [assembly: AssemblyProduct("TestWPF")] 15 | [assembly: AssemblyCopyright("Copyright © Microsoft 2012")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | //In order to begin building localizable applications, set 25 | //CultureYouAreCodingWith in your .csproj file 26 | //inside a . For example, if you are using US english 27 | //in your source files, set the to en-US. Then uncomment 28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 29 | //the line below to match the UICulture setting in the project file. 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 36 | //(used if a resource is not found in the page, 37 | // or application resource dictionaries) 38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 39 | //(used if a resource is not found in the page, 40 | // app, or any theme specific resource dictionaries) 41 | )] 42 | 43 | 44 | // Version information for an assembly consists of the following four values: 45 | // 46 | // Major Version 47 | // Minor Version 48 | // Build Number 49 | // Revision 50 | // 51 | // You can specify all the values or you can default the Build and Revision Numbers 52 | // by using the '*' as shown below: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("1.0.0.0")] 55 | [assembly: AssemblyFileVersion("1.0.0.0")] 56 | -------------------------------------------------------------------------------- /Demo/TestWPF/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestWPF.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TestWPF.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Demo/TestWPF/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Demo/TestWPF/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestWPF.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Demo/TestWPF/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Demo/TestWPF/Services.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | using Jot; 7 | using System.Windows; 8 | using System.Windows.Forms; 9 | 10 | namespace TestWPF 11 | { 12 | //this class can be replaced by the use of an IOC container 13 | static class Services 14 | { 15 | public static Tracker Tracker = new Tracker(); 16 | 17 | static Services() 18 | { 19 | Tracker 20 | .Configure() 21 | .Id(w => w.Name, SystemInformation.VirtualScreen.Size) 22 | .Properties(w => new { w.Top, w.Width, w.Height, w.Left, w.WindowState }) 23 | .PersistOn(nameof(Window.Closing)) 24 | .StopTrackingOn(nameof(Window.Closing)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Demo/TestWPF/Settings/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using Jot.Configuration; 2 | 3 | namespace TestWPF.Settings 4 | { 5 | public class AppSettings : ITrackingAware 6 | { 7 | public DisplaySettings DisplaySettings { get; set; } 8 | public GeneralSettings GeneralSettings { get; set; } 9 | 10 | public AppSettings() 11 | { 12 | DisplaySettings = new DisplaySettings(); 13 | GeneralSettings = new GeneralSettings(); 14 | } 15 | 16 | public void ConfigureTracking(TrackingConfiguration configuration) 17 | { 18 | configuration.AsGeneric().Properties(s => new { s.DisplaySettings, s.GeneralSettings }); 19 | System.Windows.Application.Current.Exit += (s, e) => { configuration.Tracker.Persist(this); }; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Demo/TestWPF/Settings/DisplaySettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.ComponentModel; 6 | using System.Windows.Media; 7 | using System.Runtime.CompilerServices; 8 | 9 | namespace TestWPF.Settings 10 | { 11 | [Serializable] 12 | public class DisplaySettings : INotifyPropertyChanged 13 | { 14 | private FontFamily _font; 15 | public FontFamily Font 16 | { 17 | get { return _font; } 18 | set { _font = value; OnPropertyChanged(); } 19 | } 20 | 21 | private decimal _fontSize = 15; 22 | public decimal FontSize 23 | { 24 | get { return _fontSize; } 25 | set { _fontSize = value; OnPropertyChanged(); } 26 | } 27 | 28 | [field: NonSerialized] 29 | public event PropertyChangedEventHandler PropertyChanged; 30 | 31 | protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) 32 | { 33 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Demo/TestWPF/Settings/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | 7 | namespace TestWPF.Settings 8 | { 9 | [Serializable] 10 | public class GeneralSettings 11 | { 12 | public int Property1 { get; set; } 13 | public string Property2 { get; set; } 14 | public IPAddress Property3 { get; set; } 15 | public bool Property4 { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/TestWPF/TestWPF.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA} 9 | WinExe 10 | Properties 11 | TestWPF 12 | TestWPF 13 | v4.7.2 14 | 15 | 16 | 512 17 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 18 | 4 19 | 20 | 21 | 22 | 23 | x86 24 | true 25 | full 26 | false 27 | bin\Debug\ 28 | DEBUG;TRACE 29 | prompt 30 | 4 31 | false 32 | 33 | 34 | x86 35 | pdbonly 36 | true 37 | bin\Release\ 38 | TRACE 39 | prompt 40 | 4 41 | false 42 | 43 | 44 | 45 | ..\..\packages\Microsoft.Extensions.Logging.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll 46 | 47 | 48 | ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 49 | 50 | 51 | 52 | ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll 53 | 54 | 55 | 56 | 57 | ..\..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll 58 | 59 | 60 | 61 | ..\..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 62 | 63 | 64 | ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 4.0 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | MSBuild:Compile 82 | Designer 83 | 84 | 85 | 86 | 87 | 88 | 89 | MSBuild:Compile 90 | Designer 91 | 92 | 93 | App.xaml 94 | Code 95 | 96 | 97 | MainWindow.xaml 98 | Code 99 | 100 | 101 | 102 | 103 | Code 104 | 105 | 106 | True 107 | True 108 | Resources.resx 109 | 110 | 111 | True 112 | Settings.settings 113 | True 114 | 115 | 116 | ResXFileCodeGenerator 117 | Resources.Designer.cs 118 | 119 | 120 | 121 | 122 | SettingsSingleFileGenerator 123 | Settings.Designer.cs 124 | 125 | 126 | 127 | 128 | 129 | {77c39ace-7180-4f7d-9b16-8408e1bc058b} 130 | Jot 131 | 132 | 133 | 134 | 141 | -------------------------------------------------------------------------------- /Demo/TestWPF/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Demo/TestWPF/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/TestWinForms/ColorPickerUC.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace TestWinForms 2 | { 3 | partial class ColorPickerUC 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.groupBox1 = new System.Windows.Forms.GroupBox(); 32 | this.pnlSample = new System.Windows.Forms.Panel(); 33 | this.lblBlue = new System.Windows.Forms.Label(); 34 | this.tbBlue = new System.Windows.Forms.TrackBar(); 35 | this.lblGreen = new System.Windows.Forms.Label(); 36 | this.tbGreen = new System.Windows.Forms.TrackBar(); 37 | this.lblRed = new System.Windows.Forms.Label(); 38 | this.tbRed = new System.Windows.Forms.TrackBar(); 39 | this.groupBox1.SuspendLayout(); 40 | ((System.ComponentModel.ISupportInitialize)(this.tbBlue)).BeginInit(); 41 | ((System.ComponentModel.ISupportInitialize)(this.tbGreen)).BeginInit(); 42 | ((System.ComponentModel.ISupportInitialize)(this.tbRed)).BeginInit(); 43 | this.SuspendLayout(); 44 | // 45 | // groupBox1 46 | // 47 | this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 48 | | System.Windows.Forms.AnchorStyles.Left) 49 | | System.Windows.Forms.AnchorStyles.Right))); 50 | this.groupBox1.Controls.Add(this.pnlSample); 51 | this.groupBox1.Controls.Add(this.lblBlue); 52 | this.groupBox1.Controls.Add(this.tbBlue); 53 | this.groupBox1.Controls.Add(this.lblGreen); 54 | this.groupBox1.Controls.Add(this.tbGreen); 55 | this.groupBox1.Controls.Add(this.lblRed); 56 | this.groupBox1.Controls.Add(this.tbRed); 57 | this.groupBox1.Location = new System.Drawing.Point(3, 3); 58 | this.groupBox1.Name = "groupBox1"; 59 | this.groupBox1.Size = new System.Drawing.Size(345, 360); 60 | this.groupBox1.TabIndex = 0; 61 | this.groupBox1.TabStop = false; 62 | this.groupBox1.Text = "groupBox1"; 63 | // 64 | // pnlSample 65 | // 66 | this.pnlSample.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 67 | | System.Windows.Forms.AnchorStyles.Left) 68 | | System.Windows.Forms.AnchorStyles.Right))); 69 | this.pnlSample.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; 70 | this.pnlSample.Location = new System.Drawing.Point(17, 178); 71 | this.pnlSample.Name = "pnlSample"; 72 | this.pnlSample.Size = new System.Drawing.Size(313, 158); 73 | this.pnlSample.TabIndex = 13; 74 | // 75 | // lblBlue 76 | // 77 | this.lblBlue.AutoSize = true; 78 | this.lblBlue.Location = new System.Drawing.Point(14, 140); 79 | this.lblBlue.Name = "lblBlue"; 80 | this.lblBlue.Size = new System.Drawing.Size(28, 13); 81 | this.lblBlue.TabIndex = 12; 82 | this.lblBlue.Text = "Blue"; 83 | // 84 | // tbBlue 85 | // 86 | this.tbBlue.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 87 | | System.Windows.Forms.AnchorStyles.Right))); 88 | this.tbBlue.BackColor = System.Drawing.Color.White; 89 | this.tbBlue.Location = new System.Drawing.Point(68, 127); 90 | this.tbBlue.Maximum = 255; 91 | this.tbBlue.Name = "tbBlue"; 92 | this.tbBlue.Size = new System.Drawing.Size(262, 45); 93 | this.tbBlue.TabIndex = 11; 94 | this.tbBlue.ValueChanged += new System.EventHandler(this.tb_ValueChanged); 95 | // 96 | // lblGreen 97 | // 98 | this.lblGreen.AutoSize = true; 99 | this.lblGreen.Location = new System.Drawing.Point(14, 89); 100 | this.lblGreen.Name = "lblGreen"; 101 | this.lblGreen.Size = new System.Drawing.Size(36, 13); 102 | this.lblGreen.TabIndex = 10; 103 | this.lblGreen.Text = "Green"; 104 | // 105 | // tbGreen 106 | // 107 | this.tbGreen.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 108 | | System.Windows.Forms.AnchorStyles.Right))); 109 | this.tbGreen.BackColor = System.Drawing.Color.White; 110 | this.tbGreen.Location = new System.Drawing.Point(68, 76); 111 | this.tbGreen.Maximum = 255; 112 | this.tbGreen.Name = "tbGreen"; 113 | this.tbGreen.Size = new System.Drawing.Size(262, 45); 114 | this.tbGreen.TabIndex = 9; 115 | this.tbGreen.ValueChanged += new System.EventHandler(this.tb_ValueChanged); 116 | // 117 | // lblRed 118 | // 119 | this.lblRed.AutoSize = true; 120 | this.lblRed.Location = new System.Drawing.Point(14, 38); 121 | this.lblRed.Name = "lblRed"; 122 | this.lblRed.Size = new System.Drawing.Size(27, 13); 123 | this.lblRed.TabIndex = 8; 124 | this.lblRed.Text = "Red"; 125 | // 126 | // tbRed 127 | // 128 | this.tbRed.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 129 | | System.Windows.Forms.AnchorStyles.Right))); 130 | this.tbRed.BackColor = System.Drawing.Color.White; 131 | this.tbRed.Location = new System.Drawing.Point(68, 25); 132 | this.tbRed.Maximum = 255; 133 | this.tbRed.Name = "tbRed"; 134 | this.tbRed.Size = new System.Drawing.Size(262, 45); 135 | this.tbRed.TabIndex = 7; 136 | this.tbRed.ValueChanged += new System.EventHandler(this.tb_ValueChanged); 137 | // 138 | // ColorPickerUC 139 | // 140 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 141 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 142 | this.Controls.Add(this.groupBox1); 143 | this.Name = "ColorPickerUC"; 144 | this.Size = new System.Drawing.Size(348, 366); 145 | this.groupBox1.ResumeLayout(false); 146 | this.groupBox1.PerformLayout(); 147 | ((System.ComponentModel.ISupportInitialize)(this.tbBlue)).EndInit(); 148 | ((System.ComponentModel.ISupportInitialize)(this.tbGreen)).EndInit(); 149 | ((System.ComponentModel.ISupportInitialize)(this.tbRed)).EndInit(); 150 | this.ResumeLayout(false); 151 | 152 | } 153 | 154 | #endregion 155 | 156 | private System.Windows.Forms.GroupBox groupBox1; 157 | private System.Windows.Forms.Panel pnlSample; 158 | private System.Windows.Forms.Label lblBlue; 159 | private System.Windows.Forms.TrackBar tbBlue; 160 | private System.Windows.Forms.Label lblGreen; 161 | private System.Windows.Forms.TrackBar tbGreen; 162 | private System.Windows.Forms.Label lblRed; 163 | private System.Windows.Forms.TrackBar tbRed; 164 | 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Demo/TestWinForms/ColorPickerUC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Windows.Forms; 4 | using Jot.Configuration; 5 | 6 | namespace TestWinForms 7 | { 8 | public partial class ColorPickerUC : UserControl, ITrackingAware 9 | { 10 | public ColorPickerUC() 11 | { 12 | InitializeComponent(); 13 | } 14 | 15 | public void ConfigureTracking(TrackingConfiguration configuration) 16 | { 17 | configuration 18 | .AsGeneric() 19 | .Id(_ => Name) 20 | .Properties(x => new { red = tbRed.Value, green = tbGreen.Value, blue = tbBlue.Value }) 21 | .PersistOn(nameof(Form.FormClosing), this.FindForm()); 22 | } 23 | 24 | protected override void OnLoad(EventArgs e) 25 | { 26 | base.OnLoad(e); 27 | groupBox1.Text = Name; 28 | } 29 | 30 | private void tb_ValueChanged(object sender, EventArgs e) 31 | { 32 | pnlSample.BackColor = Color.FromArgb(255, tbRed.Value, tbGreen.Value, tbBlue.Value); 33 | } 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /Demo/TestWinForms/ColorPickerUC.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Person.cs: -------------------------------------------------------------------------------- 1 | namespace TestWinForms 2 | { 3 | internal class Person 4 | { 5 | public string Name { get; internal set; } 6 | public string LastName { get; internal set; } 7 | public int Age { get; internal set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Demo/TestWinForms/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Windows.Forms; 5 | 6 | namespace TestWinForms 7 | { 8 | static class Program 9 | { 10 | /// 11 | /// The main entry point for the application. 12 | /// 13 | [STAThread] 14 | static void Main() 15 | { 16 | Application.EnableVisualStyles(); 17 | Application.SetCompatibleTextRenderingDefault(false); 18 | Application.Run(new Form1()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TestWinForms")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TestWinForms")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("81d41f34-27ac-4cb3-8e9c-0a454716d560")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestWinForms.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TestWinForms.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestWinForms.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/TestWinForms/Services.cs: -------------------------------------------------------------------------------- 1 | using Jot; 2 | using System.Windows.Forms; 3 | 4 | namespace TestWinForms 5 | { 6 | // tracker can be injected via an IOC container 7 | static class Services 8 | { 9 | public static Tracker Tracker = new Tracker(); 10 | 11 | static Services() 12 | { 13 | // configure tracking for all Form objects 14 | 15 | Tracker 16 | .Configure
() 17 | // use different id for different screen configurations 18 | .Id(f => f.Name, SystemInformation.VirtualScreen.Size) 19 | .Properties(f => new { f.Top, f.Width, f.Height, f.Left, f.WindowState }) 20 | .PersistOn(nameof(Form.Move), nameof(Form.Resize), nameof(Form.FormClosing)) 21 | // do not track form size and location when minimized/maximized 22 | .WhenPersistingProperty((f, p) => p.Cancel = (f.WindowState != FormWindowState.Normal && (p.Property == nameof(Form.Height) || p.Property == nameof(Form.Width) || p.Property == nameof(Form.Top) || p.Property == nameof(Form.Left)))) 23 | // a form should not be persisted after it is closed since properties will be empty 24 | .StopTrackingOn(nameof(Form.FormClosing)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Demo/TestWinForms/TestForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace TestWinForms 2 | { 3 | partial class Form1 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); 32 | this.label1 = new System.Windows.Forms.Label(); 33 | this.tabControl1 = new System.Windows.Forms.TabControl(); 34 | this.tabPage1 = new System.Windows.Forms.TabPage(); 35 | this.colorPicker2 = new TestWinForms.ColorPickerUC(); 36 | this.colorPicker1 = new TestWinForms.ColorPickerUC(); 37 | this.tabPage2 = new System.Windows.Forms.TabPage(); 38 | this.dataGridView1 = new System.Windows.Forms.DataGridView(); 39 | this.label2 = new System.Windows.Forms.Label(); 40 | this.button1 = new System.Windows.Forms.Button(); 41 | this.tabControl1.SuspendLayout(); 42 | this.tabPage1.SuspendLayout(); 43 | this.tabPage2.SuspendLayout(); 44 | ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); 45 | this.SuspendLayout(); 46 | // 47 | // label1 48 | // 49 | this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 50 | | System.Windows.Forms.AnchorStyles.Right))); 51 | this.label1.AutoEllipsis = true; 52 | this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); 53 | this.label1.Location = new System.Drawing.Point(6, 3); 54 | this.label1.Name = "label1"; 55 | this.label1.Size = new System.Drawing.Size(640, 61); 56 | this.label1.TabIndex = 2; 57 | this.label1.Text = resources.GetString("label1.Text"); 58 | // 59 | // tabControl1 60 | // 61 | this.tabControl1.Controls.Add(this.tabPage1); 62 | this.tabControl1.Controls.Add(this.tabPage2); 63 | this.tabControl1.Location = new System.Drawing.Point(12, 12); 64 | this.tabControl1.Name = "tabControl1"; 65 | this.tabControl1.SelectedIndex = 0; 66 | this.tabControl1.Size = new System.Drawing.Size(660, 418); 67 | this.tabControl1.TabIndex = 3; 68 | // 69 | // tabPage1 70 | // 71 | this.tabPage1.Controls.Add(this.colorPicker2); 72 | this.tabPage1.Controls.Add(this.colorPicker1); 73 | this.tabPage1.Controls.Add(this.label1); 74 | this.tabPage1.Location = new System.Drawing.Point(4, 22); 75 | this.tabPage1.Name = "tabPage1"; 76 | this.tabPage1.Padding = new System.Windows.Forms.Padding(3); 77 | this.tabPage1.Size = new System.Drawing.Size(652, 392); 78 | this.tabPage1.TabIndex = 0; 79 | this.tabPage1.Text = "Custom controls"; 80 | this.tabPage1.UseVisualStyleBackColor = true; 81 | // 82 | // colorPicker2 83 | // 84 | this.colorPicker2.Location = new System.Drawing.Point(353, 67); 85 | this.colorPicker2.Name = "colorPicker2"; 86 | this.colorPicker2.Size = new System.Drawing.Size(276, 302); 87 | this.colorPicker2.TabIndex = 4; 88 | // 89 | // colorPicker1 90 | // 91 | this.colorPicker1.Location = new System.Drawing.Point(21, 67); 92 | this.colorPicker1.Name = "colorPicker1"; 93 | this.colorPicker1.Size = new System.Drawing.Size(276, 302); 94 | this.colorPicker1.TabIndex = 3; 95 | // 96 | // tabPage2 97 | // 98 | this.tabPage2.Controls.Add(this.label2); 99 | this.tabPage2.Controls.Add(this.dataGridView1); 100 | this.tabPage2.Location = new System.Drawing.Point(4, 22); 101 | this.tabPage2.Name = "tabPage2"; 102 | this.tabPage2.Padding = new System.Windows.Forms.Padding(3); 103 | this.tabPage2.Size = new System.Drawing.Size(652, 392); 104 | this.tabPage2.TabIndex = 1; 105 | this.tabPage2.Text = "Data grid"; 106 | this.tabPage2.UseVisualStyleBackColor = true; 107 | // 108 | // dataGridView1 109 | // 110 | this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; 111 | this.dataGridView1.Location = new System.Drawing.Point(3, 35); 112 | this.dataGridView1.Name = "dataGridView1"; 113 | this.dataGridView1.Size = new System.Drawing.Size(643, 351); 114 | this.dataGridView1.TabIndex = 0; 115 | // 116 | // label2 117 | // 118 | this.label2.AutoSize = true; 119 | this.label2.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F); 120 | this.label2.Location = new System.Drawing.Point(6, 9); 121 | this.label2.Name = "label2"; 122 | this.label2.Size = new System.Drawing.Size(632, 17); 123 | this.label2.TabIndex = 1; 124 | this.label2.Text = "Change the widths of the columns, and restart the application. The column widths " + 125 | "will be preserved."; 126 | // 127 | // button1 128 | // 129 | this.button1.Location = new System.Drawing.Point(337, 436); 130 | this.button1.Name = "button1"; 131 | this.button1.Size = new System.Drawing.Size(171, 23); 132 | this.button1.TabIndex = 4; 133 | this.button1.Text = "Open storage folder"; 134 | this.button1.UseVisualStyleBackColor = true; 135 | this.button1.Click += new System.EventHandler(this.button1_Click); 136 | // 137 | // Form1 138 | // 139 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 140 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 141 | this.ClientSize = new System.Drawing.Size(684, 466); 142 | this.Controls.Add(this.button1); 143 | this.Controls.Add(this.tabControl1); 144 | this.Name = "Form1"; 145 | this.Text = "Form1"; 146 | this.tabControl1.ResumeLayout(false); 147 | this.tabPage1.ResumeLayout(false); 148 | this.tabPage2.ResumeLayout(false); 149 | this.tabPage2.PerformLayout(); 150 | ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit(); 151 | this.ResumeLayout(false); 152 | 153 | } 154 | 155 | #endregion 156 | private System.Windows.Forms.Label label1; 157 | private System.Windows.Forms.TabControl tabControl1; 158 | private System.Windows.Forms.TabPage tabPage1; 159 | private System.Windows.Forms.TabPage tabPage2; 160 | private ColorPickerUC colorPicker2; 161 | private ColorPickerUC colorPicker1; 162 | private System.Windows.Forms.DataGridView dataGridView1; 163 | private System.Windows.Forms.Label label2; 164 | private System.Windows.Forms.Button button1; 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /Demo/TestWinForms/TestForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Windows.Forms; 5 | using Jot.Configuration; 6 | using Jot.Storage; 7 | 8 | namespace TestWinForms 9 | { 10 | public partial class Form1 : Form, ITrackingAware 11 | { 12 | public Form1() 13 | { 14 | InitializeComponent(); 15 | 16 | dataGridView1.DataSource = new List() 17 | { 18 | new Person { Name = "Joe", LastName="Smith", Age = 34 }, 19 | new Person { Name = "Misha", LastName="Anderson", Age = 45 }, 20 | }; 21 | // NOTE: 22 | // We cannot call Track(this) in the constructor. Winfors overwrites Top/Left 23 | // properties after the constructor, so we must set them in OnLoad instead. 24 | } 25 | 26 | protected override void OnLoad(EventArgs e) 27 | { 28 | // track this form 29 | Services.Tracker.Track(this); 30 | 31 | // track color pickers as separate objects 32 | // ColorPicker also implements ITrackingAware so no configuration is needed here 33 | Services.Tracker.Track(colorPicker1); 34 | Services.Tracker.Track(colorPicker2); 35 | } 36 | 37 | public void ConfigureTracking(TrackingConfiguration configuration) 38 | { 39 | var cfg = configuration.AsGeneric(); 40 | 41 | // include selected tab index when tracking this form 42 | cfg.Property(f => f.tabControl1.SelectedIndex); 43 | 44 | // include data grid column widths when tracking this form 45 | for (int i = 0; i < dataGridView1.Columns.Count; i++) 46 | { 47 | var idx = i; // capture i into a variable (cannot use i directly since it changes in each iteration) 48 | cfg.Property(f => f.dataGridView1.Columns[idx].Width, "grid_column_" + dataGridView1.Columns[idx].Name); 49 | } 50 | } 51 | 52 | private void button1_Click(object sender, EventArgs e) 53 | { 54 | Process.Start("explorer.exe", (Services.Tracker.Store as JsonFileStore).FolderPath); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Demo/TestWinForms/TestForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Move and resize the window, play arround with the color values, then close and restart the application Notice that the location and size of the window are persisted. Also, the values of the color components are persisted, for each colorpicker individually. 122 | 123 | -------------------------------------------------------------------------------- /Demo/TestWinForms/TestWinForms.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F} 9 | WinExe 10 | Properties 11 | TestWinForms 12 | TestWinForms 13 | v4.7.2 14 | 15 | 16 | 512 17 | 18 | 19 | 20 | 21 | x86 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | x86 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | false 40 | 41 | 42 | 43 | ..\..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 44 | 45 | 46 | 47 | ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll 48 | 49 | 50 | 51 | ..\..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll 52 | 53 | 54 | 55 | ..\..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 56 | 57 | 58 | ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Form 73 | 74 | 75 | TestForm.cs 76 | 77 | 78 | 79 | 80 | 81 | UserControl 82 | 83 | 84 | ColorPickerUC.cs 85 | 86 | 87 | TestForm.cs 88 | 89 | 90 | ResXFileCodeGenerator 91 | Resources.Designer.cs 92 | Designer 93 | 94 | 95 | True 96 | Resources.resx 97 | True 98 | 99 | 100 | ColorPickerUC.cs 101 | 102 | 103 | 104 | 105 | SettingsSingleFileGenerator 106 | Settings.Designer.cs 107 | 108 | 109 | True 110 | Settings.settings 111 | True 112 | 113 | 114 | 115 | 116 | {77c39ace-7180-4f7d-9b16-8408e1bc058b} 117 | Jot 118 | 119 | 120 | 121 | 128 | -------------------------------------------------------------------------------- /Demo/TestWinForms/TestWinForms.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectFiles 5 | 6 | -------------------------------------------------------------------------------- /Demo/TestWinForms/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Demo/TestWinForms/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jot.Tests/Jot.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Jot.Tests/JsonFileStoreTests.cs: -------------------------------------------------------------------------------- 1 | using Jot.Storage; 2 | using Jot.Tests.TestData; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | using System.Text; 7 | using Xunit; 8 | 9 | namespace Jot.Tests 10 | { 11 | public class JsonFileStoreTests 12 | { 13 | JsonFileStore _store = new JsonFileStore(); 14 | 15 | [Fact] 16 | public void StoresPrimitiveData() 17 | { 18 | var data = new Dictionary() 19 | { 20 | ["Int"] = 123, 21 | ["String"] = "abc" 22 | }; 23 | 24 | _store.SetData("id", data); 25 | 26 | var data2 = _store.GetData("id"); 27 | Assert.Equal(123, data2["Int"]); 28 | Assert.Equal("abc", data2["String"]); 29 | } 30 | 31 | 32 | [Fact] 33 | public void StoresObjectGraph() 34 | { 35 | var id = Guid.NewGuid(); 36 | 37 | var data = new Dictionary() 38 | { 39 | ["Int"] = 123, 40 | ["Obj"] = new Bar() { Id = id, Str = "SomeString" } 41 | }; 42 | 43 | _store.SetData("id", data); 44 | 45 | var data2 = _store.GetData("id"); 46 | Assert.Equal(123, data2["Int"]); 47 | Assert.Equal(id, (data2["Obj"] as Bar).Id); 48 | Assert.Equal("SomeString", (data2["Obj"] as Bar).Str); 49 | } 50 | 51 | 52 | [Fact] 53 | public void StoresSpecialType_IPAddress() 54 | { 55 | var id = Guid.NewGuid(); 56 | 57 | var data = new Dictionary() 58 | { 59 | ["Int"] = 123, 60 | ["myip"] = new IPAddress(new byte[] { 1, 2, 3, 4 }) 61 | }; 62 | 63 | _store.SetData("id", data); 64 | 65 | var data2 = _store.GetData("id"); 66 | Assert.Equal(123, data2["Int"]); 67 | Assert.Equal("1.2.3.4", data2["myip"].ToString()); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/Bar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jot.Tests.TestData 4 | { 5 | class Bar 6 | { 7 | public Guid Id { get; set; } 8 | public DateTime DateTime { get; set; } 9 | public string Str { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/Foo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jot.Tests.TestData 4 | { 5 | class Foo 6 | { 7 | public int Int { get; set; } 8 | public double Double { get; set; } 9 | public TimeSpan Timespan { get; set; } 10 | public Bar A { get; set; } 11 | public Bar B { get; set; } 12 | 13 | public event EventHandler Event1; 14 | public event EventHandler Event2; 15 | 16 | public void FireEvent1() 17 | { 18 | Event1?.Invoke(this, EventArgs.Empty); 19 | } 20 | public void FireEvent2() 21 | { 22 | Event2?.Invoke(this, EventArgs.Empty); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/Foo2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jot.Tests.TestData 4 | { 5 | class Foo2 : Foo 6 | { 7 | public string DerivedFooProp1 { get; set; } 8 | public string DerivedFooProp2 { get; set; } 9 | 10 | public event EventHandler DerivedEvent; 11 | 12 | public void FireDerivedEvent1() 13 | { 14 | DerivedEvent?.Invoke(this, EventArgs.Empty); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/FooAtt.cs: -------------------------------------------------------------------------------- 1 | using Jot.Configuration.Attributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Text; 6 | 7 | namespace Jot.Tests.TestData 8 | { 9 | class FooAtt 10 | { 11 | [TrackingId] 12 | public int Int { get; set; } 13 | [Trackable] 14 | public double Double { get; set; } 15 | [Trackable, DefaultValue(3)] 16 | public double DoubleWithDefaultValueOf3 { get; set; } 17 | [Trackable] 18 | public TimeSpan Timespan { get; set; } 19 | 20 | [PersistOn] 21 | public event EventHandler RequestPersist; 22 | 23 | [StopTrackingOn] 24 | public event EventHandler StopTracking; 25 | 26 | public void FireRequest() 27 | { 28 | RequestPersist?.Invoke(this, EventArgs.Empty); 29 | 30 | } 31 | 32 | public void FireStop() 33 | { 34 | StopTracking?.Invoke(this, EventArgs.Empty); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/TestStore.cs: -------------------------------------------------------------------------------- 1 | using Jot.Storage; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Jot.Tests.TestData 7 | { 8 | class TestStore : IStore 9 | { 10 | Dictionary> data = new Dictionary>(); 11 | 12 | public void ClearAll() 13 | { 14 | data.Clear(); 15 | } 16 | 17 | public void ClearData(string id) 18 | { 19 | data.Remove(id); 20 | } 21 | 22 | public IDictionary GetData(string id) 23 | { 24 | if (data.ContainsKey(id)) 25 | return data[id]; 26 | else 27 | return null; 28 | } 29 | 30 | public IEnumerable ListIds() 31 | => data.Keys; 32 | 33 | public void SetData(string id, IDictionary values) 34 | { 35 | data[id] = values; 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Jot.Tests/TestData/TrackingAwareTestClass.cs: -------------------------------------------------------------------------------- 1 | using Jot.Configuration; 2 | 3 | namespace Jot.Tests.TestData 4 | { 5 | class TrackingAwareTestClass : Foo, ITrackingAware 6 | { 7 | public void ConfigureTracking(TrackingConfiguration configuration) 8 | { 9 | configuration.AsGeneric() 10 | .Id(f => "x") 11 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Jot.Tests/TrackerTests.cs: -------------------------------------------------------------------------------- 1 | using Jot.Configuration; 2 | using Jot.Storage; 3 | using Jot.Tests.TestData; 4 | using Moq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace Jot.Tests 11 | { 12 | public partial class TrackerShould 13 | { 14 | Tracker _tracker; 15 | TestStore _store = new TestStore(); 16 | 17 | public TrackerShould() 18 | { 19 | _tracker = new Tracker(_store); 20 | } 21 | 22 | [Fact] 23 | public void DoNothing_IfNoSavedData() 24 | { 25 | var testData = new Foo() { Double = -99.9f }; 26 | _tracker.Configure().Properties(f => new { f.Double }); 27 | _tracker.Track(testData); 28 | Assert.Equal(-99.9f, testData.Double); 29 | } 30 | 31 | [Fact] 32 | public void ApplyDefaultValues_IfProvidedAndNoSavedData() 33 | { 34 | _tracker.Configure() 35 | .Id(x => "some new id") 36 | .Property(f => f.Double, 123, "myprop") 37 | .Property(f => f.Timespan, new TimeSpan(11, 22, 33), "2"); 38 | 39 | var testData = new Foo(); 40 | _tracker.Track(testData); 41 | 42 | Assert.Equal(123, testData.Double); 43 | Assert.Equal(new TimeSpan(11, 22, 33), testData.Timespan); 44 | } 45 | 46 | [Fact] 47 | public void TrackPrimitiveProperties() 48 | { 49 | // save some data 50 | var testData1 = new Foo() { Double = 123.45, Int = 456, Timespan = new TimeSpan(99, 99, 99) }; 51 | _tracker 52 | .Configure() 53 | .Id(f => "x") 54 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 55 | _tracker.Track(testData1); 56 | _tracker.Persist(testData1); 57 | 58 | // simulate application restart and read the saved data 59 | _tracker = new Tracker(_store); 60 | 61 | var testData2 = new Foo(); 62 | _tracker.Configure() 63 | .Id(f => "x") 64 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 65 | _tracker.Track(testData2); 66 | 67 | // verify the original data object and the restored data object are the same 68 | Assert.Equal(testData1.Double, testData2.Double); 69 | Assert.Equal(testData1.Int, testData2.Int); 70 | Assert.Equal(testData1.Timespan, testData2.Timespan); 71 | } 72 | 73 | [Fact] 74 | public void TestForget() 75 | { 76 | // save some data 77 | var testData1 = new Foo() { Double = 123.45, Int = 456, Timespan = new TimeSpan(99, 99, 99) }; 78 | _tracker 79 | .Configure() 80 | .Id(f => "x") 81 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 82 | _tracker.Track(testData1); 83 | _tracker.Persist(testData1); 84 | 85 | // simulate application restart and read the saved data 86 | _tracker = new Tracker(_store); 87 | _tracker.Configure() 88 | .Id(f => "x") 89 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 90 | 91 | 92 | var testData2 = new Foo(); 93 | 94 | // forget 95 | _tracker.Forget(testData2); 96 | 97 | _tracker.Track(testData2); 98 | 99 | // verify the data from the original object did not survive 100 | Assert.Equal(default(double), testData2.Double); 101 | Assert.Equal(default(int), testData2.Int); 102 | Assert.Equal(default(TimeSpan), testData2.Timespan); 103 | } 104 | 105 | [Fact] 106 | public void HonorITrackingAware() 107 | { 108 | // save some data 109 | var testData1 = new TrackingAwareTestClass() 110 | { 111 | Double = 123.45, 112 | Int = 456, 113 | Timespan = new TimeSpan(99, 99, 99) 114 | }; 115 | _tracker.Track(testData1); 116 | _tracker.Persist(testData1); 117 | 118 | // simulate application restart and read the saved data 119 | _tracker = new Tracker(_store); 120 | 121 | var testData2 = new TrackingAwareTestClass(); 122 | _tracker.Track(testData2); 123 | 124 | // verify the original data object and the restored data object are the same 125 | Assert.Equal(testData1.Double, testData2.Double); 126 | Assert.Equal(testData1.Int, testData2.Int); 127 | Assert.Equal(testData1.Timespan, testData2.Timespan); 128 | } 129 | 130 | [Fact] 131 | public void TrackOnlySelectedProperties() 132 | { 133 | //save some data 134 | var testData1 = new Foo() { Double = 123.45, Int = 456, Timespan = new TimeSpan(99, 99, 99) }; 135 | _tracker 136 | .Configure() 137 | .Id(f => "x") 138 | .Properties(f => new { f.Double, f.Int }); 139 | _tracker.Track(testData1); 140 | 141 | // simulate application restart and read the saved data 142 | _tracker.PersistAll(); 143 | _tracker = new Tracker(_store); 144 | 145 | var testData2 = new Foo(); 146 | _tracker.Configure() 147 | .Id(f => "x") 148 | .Properties(f => new { f.Double, f.Int }); 149 | _tracker.Track(testData2); 150 | 151 | Assert.Equal(testData1.Double, testData2.Double); 152 | Assert.Equal(testData1.Int, testData2.Int); 153 | Assert.Equal(testData2.Timespan, new TimeSpan(0, 0, 0));//we did not track the TimeSpan property 154 | 155 | } 156 | 157 | [Fact] 158 | public void DetectsAttributes() 159 | { 160 | _tracker.Configure(); 161 | 162 | //save some data 163 | var testData1 = new FooAtt() { Double = 123.45, Int = 456, Timespan = new TimeSpan(99, 99, 99) }; 164 | _tracker.Track(testData1); 165 | testData1.Double = 888; 166 | testData1.FireRequest(); 167 | testData1.FireStop(); 168 | testData1.Double = 777; 169 | testData1.FireRequest(); 170 | 171 | 172 | // simulate application restart and read the saved data 173 | _tracker = new Tracker(_store); 174 | var testData2 = new FooAtt() { Int = 456 }; 175 | _tracker.Configure(); 176 | _tracker.Track(testData2); 177 | 178 | Assert.Equal(888, testData2.Double); 179 | Assert.Equal(testData1.Int, testData2.Int); 180 | Assert.Equal(testData2.Timespan, new TimeSpan(99, 99, 99)); 181 | } 182 | 183 | [Fact] 184 | public void DetectsDefaultValueAttribute() 185 | { 186 | //save some data 187 | var testData1 = new FooAtt(); 188 | _tracker.Configure().Track(testData1); 189 | Assert.Equal(3, testData1.DoubleWithDefaultValueOf3); 190 | } 191 | 192 | [Fact] 193 | public void NotUseOtherObjectsData() 194 | { 195 | //save some data 196 | var testData1 = new Foo() { Double = 123.45, Int = 456, Timespan = new TimeSpan(99, 99, 99) }; 197 | _tracker 198 | .Configure() 199 | .Id(f => "x") 200 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 201 | _tracker.Track(testData1); 202 | 203 | // simulate application restart and read the saved data 204 | _tracker = new Tracker(_store); 205 | 206 | var testData2 = new Foo(); 207 | _tracker.Configure() 208 | .Id(f => "not the same id") 209 | .Properties(f => new { f.Double, f.Int, f.Timespan }); 210 | _tracker.Track(testData2); 211 | 212 | //verify we did not get data from object "x" 213 | Assert.Equal(0, testData2.Double); 214 | Assert.Equal(0, testData2.Int); 215 | } 216 | 217 | [Fact] 218 | public void PersistTrackedObjects_WhenPersistAllCalled() 219 | { 220 | //The idea: verify that firing the PersistRequired event causes the data store to commit the data 221 | 222 | Mock storeMoq = new Mock(); 223 | 224 | var tracker = new Tracker(storeMoq.Object); 225 | tracker.Configure().Properties(f => new { f.Double, f.Int }).Id(f => "abc");//add initializer 226 | 227 | var testData1 = new Foo() { Double = 123.45f, Int = 456 }; 228 | 229 | //verify changes were committed once the persist trigger was fired 230 | storeMoq.Verify(s => s.SetData(It.IsAny(), It.IsAny>()), Times.Never); 231 | tracker.PersistAll(); 232 | storeMoq.Verify(s => s.SetData(It.IsAny(), It.IsAny>()), Times.Never); 233 | } 234 | 235 | [Fact] 236 | public void ReturnSameConfig_IfSameTarget() 237 | { 238 | // note: 239 | // the generic version would generate different instances, 240 | // but the generic instances are just throw-away wrappers 241 | // around the non-generic TrackingConfiguration 242 | 243 | var cfg1 = _tracker.Configure(typeof(Foo)); 244 | var cfg2 = _tracker.Configure(typeof(Foo)); 245 | 246 | Assert.Same(cfg1, cfg2); 247 | } 248 | 249 | [Fact] 250 | public void Persist_WhenCalled() 251 | { 252 | var cfg1 = _tracker.Configure() 253 | .Id(f => f.Int.ToString(), includeType: false) 254 | .Properties(f => new { f.B, x = f.Timespan }) 255 | .PersistOn(nameof(Foo.Event1)); 256 | 257 | var cfg2 = _tracker.Configure() 258 | .PersistOn(nameof(Foo2.DerivedEvent)) 259 | .Properties(f2 => new { f2.DerivedFooProp1 }); 260 | 261 | var foo2 = new Foo2() 262 | { 263 | Double = 123, 264 | Int = 321, 265 | Timespan = new TimeSpan(1, 2, 3), 266 | DerivedFooProp1 = "str1", 267 | DerivedFooProp2 = "str2", 268 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 269 | }; 270 | 271 | _tracker.Track(foo2); 272 | _tracker.Persist(foo2); 273 | 274 | Assert.NotSame(cfg1, cfg2); 275 | 276 | var data = _store.GetData(foo2.Int.ToString()); 277 | Assert.Equal(3, data.Count); 278 | Assert.Equal(foo2.DerivedFooProp1, data["DerivedFooProp1"]); 279 | Assert.Equal(foo2.Timespan, data["x"]); 280 | Assert.Equal(foo2.B.Id, (data["B"] as Bar).Id); 281 | Assert.Equal(foo2.B.Str, (data["B"] as Bar).Str); 282 | } 283 | 284 | 285 | [Fact] 286 | public void HonorsIdNamespace() 287 | { 288 | var cfg1 = _tracker.Configure() 289 | .Id(f => f.Int.ToString(), "context1", includeType: false) 290 | .Properties(f => new { f.B, x = f.Timespan }) 291 | .PersistOn(nameof(Foo.Event1)); 292 | 293 | var foo = new Foo() 294 | { 295 | Double = 123, 296 | Int = 321, 297 | Timespan = new TimeSpan(1, 2, 3), 298 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 299 | }; 300 | 301 | _tracker.Track(foo); 302 | _tracker.Persist(foo); 303 | 304 | var data = _store.GetData("context1.321"); 305 | Assert.Equal(2, data.Count); 306 | Assert.Equal(foo.Timespan, data["x"]); 307 | Assert.Equal(foo.B.Id, (data["B"] as Bar).Id); 308 | Assert.Equal(foo.B.Str, (data["B"] as Bar).Str); 309 | } 310 | 311 | [Fact] 312 | public void PersistNestedProperties() 313 | { 314 | var cfg1 = _tracker.Configure() 315 | .Id(f => f.Int.ToString(), includeType: false) 316 | .Properties(f => new { f.B.Str, x = f.Timespan }) 317 | .PersistOn(nameof(Foo.Event1)); 318 | 319 | var foo2 = new Foo2() 320 | { 321 | Double = 123, 322 | Int = 321, 323 | Timespan = new TimeSpan(1, 2, 3), 324 | DerivedFooProp1 = "str1", 325 | DerivedFooProp2 = "str2", 326 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 327 | }; 328 | 329 | _tracker.Track(foo2); 330 | _tracker.Persist(foo2); 331 | 332 | var data = _store.GetData(foo2.Int.ToString()); 333 | Assert.Equal(2, data.Count); 334 | Assert.Equal(foo2.Timespan, data["x"]); 335 | Assert.Equal(foo2.B.Str, data["Str"]); 336 | } 337 | 338 | 339 | [Fact] 340 | public void PersistNestedPropertiesWithDynamicNames() 341 | { 342 | var cfg1 = _tracker.Configure() 343 | .Id(f => f.Int.ToString(), includeType: false) 344 | .Properties(f => new { x = f.Timespan }) 345 | .PersistOn(nameof(Foo.Event1)); 346 | 347 | cfg1.Property(f => f.B.Str, "0"); 348 | 349 | var foo2 = new Foo2() 350 | { 351 | Double = 123, 352 | Int = 321, 353 | Timespan = new TimeSpan(1, 2, 3), 354 | DerivedFooProp1 = "str1", 355 | DerivedFooProp2 = "str2", 356 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 357 | }; 358 | 359 | _tracker.Track(foo2); 360 | _tracker.Persist(foo2); 361 | 362 | var data = _store.GetData(foo2.Int.ToString()); 363 | Assert.Equal(2, data.Count); 364 | Assert.Equal(foo2.Timespan, data["x"]); 365 | Assert.Equal(foo2.B.Str, data["0"]); 366 | } 367 | 368 | [Fact] 369 | public void Persist_UsingBaseConfiguration() 370 | { 371 | // 1. arrange 372 | // prepare cfg for Foo 373 | _tracker.Configure() 374 | .Id(f => f.Int.ToString(), includeType: false) 375 | .Properties(f => new { f.B }) 376 | .PersistOn(nameof(Foo.Event1)); 377 | // create Foo2 instance (derived from Foo) 378 | var foo2 = new Foo2() 379 | { 380 | Double = 123, 381 | Int = 321, 382 | Timespan = new TimeSpan(1, 2, 3), 383 | DerivedFooProp1 = "str1", 384 | DerivedFooProp2 = "str2", 385 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 386 | }; 387 | 388 | // 2. act (start tracking and call persist) 389 | _tracker.Track(foo2); 390 | _tracker.Persist(foo2); 391 | 392 | // 3. assert (properties set in base cfg are saved) 393 | var data = _store.GetData(foo2.Int.ToString()); 394 | Assert.Equal(1, data.Count); 395 | Assert.Equal(foo2.B.Id, (data["B"] as Bar).Id); 396 | Assert.Equal(foo2.B.Str, (data["B"] as Bar).Str); 397 | } 398 | 399 | [Fact] 400 | public void Persist_WhenDerivedEventFired() 401 | { 402 | var cfg1 = _tracker.Configure() 403 | .Id(f => f.Int.ToString(), includeType: false) 404 | .Properties(f => new { f.B, x = f.Timespan }) 405 | .PersistOn(nameof(Foo.Event1)); 406 | 407 | var cfg2 = _tracker.Configure() 408 | .PersistOn(nameof(Foo2.DerivedEvent)); 409 | 410 | var foo2 = new Foo2() 411 | { 412 | Double = 123, 413 | Int = 321, 414 | Timespan = new TimeSpan(1, 2, 3), 415 | DerivedFooProp1 = "str1", 416 | DerivedFooProp2 = "str2", 417 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 418 | }; 419 | 420 | _tracker.Track(foo2); 421 | foo2.FireDerivedEvent1(); 422 | 423 | Assert.NotSame(cfg1, cfg2); 424 | 425 | var data = _store.GetData(foo2.Int.ToString()); 426 | Assert.Equal(2, data.Count); 427 | Assert.Equal(foo2.Timespan, data["x"]); 428 | Assert.Equal(foo2.B.Id, (data["B"] as Bar).Id); 429 | Assert.Equal(foo2.B.Str, (data["B"] as Bar).Str); 430 | } 431 | 432 | [Fact] 433 | public void Persist_BaseAndOwnedProperties() 434 | { 435 | var cfg1 = _tracker.Configure() 436 | .Id(f => f.Int.ToString(), includeType: false) 437 | .Properties(f => new { f.B, x = f.Timespan }) 438 | .PersistOn(nameof(Foo.Event1)); 439 | 440 | var cfg2 = _tracker.Configure() 441 | .PersistOn(nameof(Foo2.DerivedEvent)) 442 | .Property(f2 => f2.DerivedFooProp1); 443 | 444 | var foo2 = new Foo2() 445 | { 446 | Double = 123, 447 | Int = 321, 448 | Timespan = new TimeSpan(1, 2, 3), 449 | DerivedFooProp1 = "str1", 450 | DerivedFooProp2 = "str2", 451 | B = new Bar() { Id = Guid.NewGuid(), Str = "BarStr" } 452 | }; 453 | 454 | _tracker.Track(foo2); 455 | _tracker.Persist(foo2); 456 | 457 | Assert.NotSame(cfg1, cfg2); 458 | 459 | var data = _store.GetData(foo2.Int.ToString()); 460 | Assert.Equal(3, data.Count); 461 | Assert.Equal(foo2.DerivedFooProp1, data["f2.DerivedFooProp1"]); 462 | Assert.Equal(foo2.Timespan, data["x"]); 463 | Assert.Equal(foo2.B.Id, (data["B"] as Bar).Id); 464 | Assert.Equal(foo2.B.Str, (data["B"] as Bar).Str); 465 | } 466 | 467 | 468 | [Fact] 469 | public void StopTracking_WhenRequested() 470 | { 471 | var cfg1 = _tracker.Configure() 472 | .Id(f => f.Int.ToString(), includeType: false) 473 | .Properties(f => new { f.Double }) 474 | .PersistOn(nameof(Foo.Event1)); 475 | 476 | var foo = new Foo() 477 | { 478 | Int = 321, 479 | Double = 444 480 | }; 481 | 482 | _tracker.Track(foo); 483 | _tracker.Persist(foo); 484 | 485 | // stop tracking 486 | _tracker.StopTracking(foo); 487 | 488 | foo.Double = 555; 489 | 490 | // normally would cause tracking 491 | foo.FireEvent1(); 492 | 493 | var data = _store.GetData(foo.Int.ToString()); 494 | Assert.Equal(444.0, data["Double"]); 495 | } 496 | 497 | [Fact] 498 | public void StopTracking_OnEvent() 499 | { 500 | var cfg1 = _tracker.Configure() 501 | .Id(f => f.Int.ToString(), includeType: false) 502 | .Properties(f => new { f.Double }) 503 | .PersistOn(nameof(Foo.Event1)) 504 | .StopTrackingOn(nameof(Foo.Event2)); 505 | 506 | var foo = new Foo() 507 | { 508 | Int = 321, 509 | Double = 444 510 | }; 511 | 512 | _tracker.Track(foo); 513 | _tracker.Persist(foo); 514 | 515 | // stop tracking 516 | foo.FireEvent2(); 517 | 518 | foo.Double = 555; 519 | 520 | // normally would cause tracking 521 | foo.FireEvent1(); 522 | 523 | var data = _store.GetData(foo.Int.ToString()); 524 | Assert.Equal(444.0, data["Double"]); 525 | } 526 | 527 | [Fact] 528 | public void StopTracking_OnEventOtherObject() 529 | { 530 | var fooOther = new Foo(); 531 | 532 | var cfg1 = _tracker.Configure() 533 | .Id(f => f.Int.ToString(), includeType: false) 534 | .Properties(f => new { f.Double }) 535 | .PersistOn(nameof(Foo.Event1)) 536 | .StopTrackingOn(nameof(Foo.Event2), fooOther); 537 | 538 | var foo = new Foo() 539 | { 540 | Int = 321, 541 | Double = 444 542 | }; 543 | 544 | _tracker.Track(foo); 545 | _tracker.Persist(foo); 546 | 547 | // stop tracking 548 | fooOther.FireEvent2(); 549 | 550 | foo.Double = 555; 551 | 552 | // normally would cause tracking 553 | foo.FireEvent1(); 554 | 555 | var data = _store.GetData(foo.Int.ToString()); 556 | Assert.Equal(444.0, data["Double"]); 557 | } 558 | 559 | [Fact] 560 | public void CallApplyingHandler() 561 | { 562 | List> callsLog = new List>(); 563 | _tracker.Configure() 564 | .Id(f => f.Int.ToString(), includeType: false) 565 | .Properties(f => new { f.Double, f.Timespan }) 566 | .WhenApplyingProperty((f, pd) => callsLog.Add(new Tuple(f, pd))); 567 | 568 | _store.SetData("321", new Dictionary { ["Double"] = 444, ["Timespan"] = new TimeSpan(1, 2, 3) }); 569 | 570 | var foo = new Foo() 571 | { 572 | Int = 321, 573 | }; 574 | 575 | _tracker.Track(foo); 576 | 577 | Assert.Equal(2, callsLog.Count); 578 | Assert.Equal(foo, callsLog[0].Item1); 579 | Assert.Equal("Double", callsLog[0].Item2.Property); 580 | Assert.Equal(444, callsLog[0].Item2.Value); 581 | Assert.Equal(foo, callsLog[1].Item1); 582 | Assert.Equal("Timespan", callsLog[1].Item2.Property); 583 | Assert.Equal(new TimeSpan(1, 2, 3), callsLog[1].Item2.Value); 584 | } 585 | 586 | [Fact] 587 | public void CancelApplyingWhenRequested() 588 | { 589 | _tracker.Configure() 590 | .Id(f => f.Int.ToString(), includeType: false) 591 | .Properties(f => new { f.Double, f.Timespan }) 592 | .WhenApplyingProperty((f, pd) => pd.Cancel = pd.Property == "Double"); 593 | 594 | _store.SetData("321", new Dictionary { ["Double"] = 444, ["Timespan"] = new TimeSpan(1, 2, 3) }); 595 | 596 | var foo = new Foo() 597 | { 598 | Int = 321, 599 | }; 600 | 601 | _tracker.Track(foo); 602 | 603 | Assert.Equal(default(double), foo.Double); 604 | Assert.Equal(new TimeSpan(1, 2, 3), foo.Timespan); 605 | } 606 | 607 | [Fact] 608 | public void NotifyWhenApplied() 609 | { 610 | List callsLog = new List(); 611 | _tracker.Configure() 612 | .Id(f => f.Int.ToString()) 613 | .Properties(f => new { f.Double, f.Timespan }) 614 | .WhenAppliedState(f => callsLog.Add(f)); 615 | 616 | _store.SetData("321", new Dictionary { ["Double"] = 444, ["Timespan"] = new TimeSpan(1, 2, 3) }); 617 | 618 | var foo = new Foo() 619 | { 620 | Int = 321, 621 | }; 622 | 623 | Assert.Empty(callsLog); 624 | _tracker.Track(foo); 625 | Assert.Equal(foo, callsLog.Single()); 626 | } 627 | 628 | 629 | [Fact] 630 | public void CallPersistingHandler() 631 | { 632 | List> callsLog = new List>(); 633 | _tracker.Configure() 634 | .Id(f => f.Int.ToString()) 635 | .Properties(f => new { f.Double, f.Timespan }) 636 | .WhenPersistingProperty((f, pd) => callsLog.Add(new Tuple(f, pd))); 637 | 638 | var foo = new Foo() 639 | { 640 | Int = 321, 641 | Double = 444, 642 | Timespan = new TimeSpan(1, 2, 3) 643 | }; 644 | 645 | _tracker.Track(foo); 646 | _tracker.Persist(foo); 647 | 648 | Assert.Equal(2, callsLog.Count); 649 | Assert.Equal(foo, callsLog[0].Item1); 650 | Assert.Equal("Double", callsLog[0].Item2.Property); 651 | Assert.Equal(444.0, callsLog[0].Item2.Value); 652 | Assert.Equal(foo, callsLog[1].Item1); 653 | Assert.Equal("Timespan", callsLog[1].Item2.Property); 654 | Assert.Equal(new TimeSpan(1, 2, 3), callsLog[1].Item2.Value); 655 | } 656 | [Fact] 657 | public void CancelPersistingWhenRequested() 658 | { 659 | // arrange (set up cancel for property == Double) 660 | _tracker.Configure() 661 | .Id(f => f.Int.ToString(), includeType: false) 662 | .Properties(f => new { f.Double, f.Timespan }) 663 | .WhenPersistingProperty((f, pd) => pd.Cancel = pd.Property == "Double"); 664 | 665 | // act (persist) 666 | var foo = new Foo() 667 | { 668 | Int = 321, 669 | Double = 444, 670 | Timespan = new TimeSpan(1, 2, 3) 671 | }; 672 | _tracker.Track(foo); 673 | _tracker.Persist(foo); 674 | 675 | // assert (property "Double" is not persisted, but "Timespan" is) 676 | var data = _store.GetData("321"); 677 | Assert.Equal(1, data.Count); 678 | Assert.Equal(new TimeSpan(1, 2, 3), data["Timespan"]); 679 | } 680 | [Fact] 681 | public void NotifyWhenPersisted() 682 | { 683 | List callsLog = new List(); 684 | _tracker.Configure() 685 | .Id(f => f.Int.ToString()) 686 | .Properties(f => new { f.Double, f.Timespan }) 687 | .WhenPersisted(f => callsLog.Add(f)); 688 | 689 | _store.SetData("321", new Dictionary { ["Double"] = 444, ["Timespan"] = new TimeSpan(1, 2, 3) }); 690 | 691 | var foo = new Foo() 692 | { 693 | Int = 321, 694 | }; 695 | _tracker.Track(foo); 696 | 697 | Assert.Empty(callsLog); 698 | _tracker.Persist(foo); 699 | Assert.Equal(foo, callsLog.Single()); 700 | } 701 | 702 | // + properties are merged 703 | // + persists when requested 704 | // + stops tracking when reqested 705 | // + stops tracking when reqested by other object 706 | // notifies when applied 707 | // notifies when persisted 708 | // cancels applying when needed 709 | // cancels persisting when needed 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /Jot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29326.143 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{A3B3B8F6-A266-4363-9647-991928B06C5B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWinForms", "Demo\TestWinForms\TestWinForms.csproj", "{216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWPF", "Demo\TestWPF\TestWPF.csproj", "{9D1520D9-DE96-4C8D-8C19-FA921851C9DA}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6970362B-C7F2-40F1-8BC8-882A0BEF0A40}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{A376E1B4-CE6F-44C8-A008-72EF8E1F6909}" 15 | ProjectSection(SolutionItems) = preProject 16 | .gitignore = .gitignore 17 | LICENSE.txt = LICENSE.txt 18 | README.md = README.md 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jot", "Jot\Jot.csproj", "{77C39ACE-7180-4F7D-9B16-8408E1BC058B}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jot.Tests", "Jot.Tests\Jot.Tests.csproj", "{E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Debug|Mixed Platforms = Debug|Mixed Platforms 29 | Debug|x86 = Debug|x86 30 | Deploy|Any CPU = Deploy|Any CPU 31 | Deploy|Mixed Platforms = Deploy|Mixed Platforms 32 | Deploy|x86 = Deploy|x86 33 | Release|Any CPU = Release|Any CPU 34 | Release|Mixed Platforms = Release|Mixed Platforms 35 | Release|x86 = Release|x86 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Debug|Any CPU.ActiveCfg = Debug|x86 39 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 40 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Debug|Mixed Platforms.Build.0 = Debug|x86 41 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Debug|x86.ActiveCfg = Debug|x86 42 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Debug|x86.Build.0 = Debug|x86 43 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Deploy|Any CPU.ActiveCfg = Debug|x86 44 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Deploy|Mixed Platforms.ActiveCfg = Debug|x86 45 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Deploy|Mixed Platforms.Build.0 = Debug|x86 46 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Deploy|x86.ActiveCfg = Debug|x86 47 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Deploy|x86.Build.0 = Debug|x86 48 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Release|Any CPU.ActiveCfg = Release|x86 49 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Release|Mixed Platforms.ActiveCfg = Release|x86 50 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Release|Mixed Platforms.Build.0 = Release|x86 51 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Release|x86.ActiveCfg = Release|x86 52 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F}.Release|x86.Build.0 = Release|x86 53 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Debug|Any CPU.ActiveCfg = Debug|x86 54 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 55 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Debug|Mixed Platforms.Build.0 = Debug|x86 56 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Debug|x86.ActiveCfg = Debug|x86 57 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Debug|x86.Build.0 = Debug|x86 58 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Deploy|Any CPU.ActiveCfg = Debug|x86 59 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Deploy|Mixed Platforms.ActiveCfg = Debug|x86 60 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Deploy|Mixed Platforms.Build.0 = Debug|x86 61 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Deploy|x86.ActiveCfg = Debug|x86 62 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Deploy|x86.Build.0 = Debug|x86 63 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Release|Any CPU.ActiveCfg = Release|x86 64 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Release|Mixed Platforms.ActiveCfg = Release|x86 65 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Release|Mixed Platforms.Build.0 = Release|x86 66 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Release|x86.ActiveCfg = Release|x86 67 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA}.Release|x86.Build.0 = Release|x86 68 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 71 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 72 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|x86.ActiveCfg = Debug|Any CPU 73 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Debug|x86.Build.0 = Debug|Any CPU 74 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|Any CPU.ActiveCfg = Debug|Any CPU 75 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|Any CPU.Build.0 = Debug|Any CPU 76 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|Mixed Platforms.ActiveCfg = Debug|Any CPU 77 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|Mixed Platforms.Build.0 = Debug|Any CPU 78 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|x86.ActiveCfg = Debug|Any CPU 79 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Deploy|x86.Build.0 = Debug|Any CPU 80 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|Any CPU.ActiveCfg = Release|Any CPU 81 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|Any CPU.Build.0 = Release|Any CPU 82 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 83 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|Mixed Platforms.Build.0 = Release|Any CPU 84 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|x86.ActiveCfg = Release|Any CPU 85 | {77C39ACE-7180-4F7D-9B16-8408E1BC058B}.Release|x86.Build.0 = Release|Any CPU 86 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 87 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|Any CPU.Build.0 = Debug|Any CPU 88 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 89 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 90 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|x86.ActiveCfg = Debug|Any CPU 91 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Debug|x86.Build.0 = Debug|Any CPU 92 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|Any CPU.ActiveCfg = Debug|Any CPU 93 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|Any CPU.Build.0 = Debug|Any CPU 94 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|Mixed Platforms.ActiveCfg = Debug|Any CPU 95 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|Mixed Platforms.Build.0 = Debug|Any CPU 96 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|x86.ActiveCfg = Debug|Any CPU 97 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Deploy|x86.Build.0 = Debug|Any CPU 98 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|Any CPU.ActiveCfg = Release|Any CPU 99 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|Any CPU.Build.0 = Release|Any CPU 100 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 101 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|Mixed Platforms.Build.0 = Release|Any CPU 102 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|x86.ActiveCfg = Release|Any CPU 103 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D}.Release|x86.Build.0 = Release|Any CPU 104 | EndGlobalSection 105 | GlobalSection(SolutionProperties) = preSolution 106 | HideSolutionNode = FALSE 107 | EndGlobalSection 108 | GlobalSection(NestedProjects) = preSolution 109 | {216ABFAE-1495-49BD-9C2F-0C4C3F09ED6F} = {A3B3B8F6-A266-4363-9647-991928B06C5B} 110 | {9D1520D9-DE96-4C8D-8C19-FA921851C9DA} = {A3B3B8F6-A266-4363-9647-991928B06C5B} 111 | {E6FB29A1-C2C1-4CE2-8510-4FF299EBAF2D} = {6970362B-C7F2-40F1-8BC8-882A0BEF0A40} 112 | EndGlobalSection 113 | GlobalSection(ExtensibilityGlobals) = postSolution 114 | SolutionGuid = {3D6CF7E1-1FAC-40E4-8230-16CB6F0B061F} 115 | EndGlobalSection 116 | EndGlobal 117 | -------------------------------------------------------------------------------- /Jot/Configuration/Attributes/PersistTriggerAttributete.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Jot.Configuration.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.Event)] 8 | public class PersistOnAttribute : Attribute 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jot/Configuration/Attributes/StopTrackingTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Jot.Configuration.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.Event)] 8 | public class StopTrackingOnAttribute : Attribute 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jot/Configuration/Attributes/TrackableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Jot.Configuration.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 8 | public class TrackableAttribute : Attribute 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jot/Configuration/Attributes/TrackingKeyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Jot.Configuration.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.Property)] 8 | public class TrackingIdAttribute : Attribute 9 | { 10 | public bool IncludeType { get; } 11 | 12 | public object Namespace { get; } 13 | 14 | public TrackingIdAttribute(object @namespace = null, bool includeType = false) 15 | { 16 | IncludeType = includeType; 17 | Namespace = @namespace; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jot/Configuration/ITrackingAware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Jot.Configuration 6 | { 7 | public interface ITrackingAware 8 | { 9 | /// 10 | /// Allows an object to configure its tracking. 11 | /// 12 | /// 13 | void ConfigureTracking(TrackingConfiguration configuration); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jot/Configuration/ITrackingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace Jot.Configuration 6 | { 7 | public interface ITrackingConfiguration 8 | { 9 | List PersistTriggers { get; } 10 | Trigger StopTrackingTrigger { get; set; } 11 | Type TargetType { get; } 12 | Dictionary TrackedProperties { get; } 13 | Tracker Tracker { get; } 14 | 15 | ITrackingConfiguration CanPersist(Func canPersistFunc); 16 | string GetStoreId(object target); 17 | ITrackingConfiguration Id(Func idFunc, object @namespace = null, bool includeType = true); 18 | ITrackingConfiguration PersistOn(params string[] eventNames); 19 | ITrackingConfiguration PersistOn(string eventName, Func eventSourceGetter); 20 | ITrackingConfiguration PersistOn(string eventName, object eventSourceObject); 21 | ITrackingConfiguration StopTrackingOn(string eventName); 22 | ITrackingConfiguration StopTrackingOn(string eventName, Func eventSourceGetter); 23 | ITrackingConfiguration StopTrackingOn(string eventName, object eventSource); 24 | ITrackingConfiguration WhenAppliedState(Action action); 25 | ITrackingConfiguration WhenApplyingProperty(Action action); 26 | ITrackingConfiguration WhenPersisted(Action action); 27 | ITrackingConfiguration WhenPersistingProperty(Action action); 28 | } 29 | } -------------------------------------------------------------------------------- /Jot/Configuration/TrackedPropertyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jot.Configuration 4 | { 5 | /// 6 | /// An object that decribes the tracking information for a target object's property. 7 | /// 8 | public class TrackedPropertyInfo 9 | { 10 | /// 11 | /// Function that gets the value of the property. 12 | /// 13 | public Func Getter { get; } 14 | /// 15 | /// Action that sets the value of the property. 16 | /// 17 | public Action Setter { get; } 18 | /// 19 | /// Indicates if a default value is provided for the property. 20 | /// 21 | public bool IsDefaultSpecified { get; } 22 | /// 23 | /// The value that will be applied to a tracked property if no existing persisted data is found. 24 | /// 25 | public object DefaultValue { get; } 26 | 27 | internal TrackedPropertyInfo(Func getter, Action setter) 28 | : this(getter, setter, null) 29 | { 30 | IsDefaultSpecified = false; 31 | } 32 | 33 | internal TrackedPropertyInfo(Func getter, Action setter, object defaultValue) 34 | { 35 | Getter = getter; 36 | Setter = setter; 37 | IsDefaultSpecified = true; 38 | DefaultValue = defaultValue; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Jot/Configuration/TrackingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Diagnostics; 6 | using System.Linq.Expressions; 7 | using System.Text; 8 | using Jot.Configuration.Attributes; 9 | using System.ComponentModel; 10 | 11 | namespace Jot.Configuration 12 | { 13 | /// 14 | /// A TrackingConfiguration is an object that determines how a target object will be tracked. 15 | /// 16 | public class TrackingConfiguration : ITrackingConfiguration 17 | { 18 | public Type TargetType { get; } 19 | 20 | Func idFunc; 21 | Func canPersistFunc = x => true; 22 | 23 | Action applyingPropertyAction; 24 | Action persistingPropertyAction; 25 | Action appliedAction; 26 | Action persistedAction; 27 | 28 | /// 29 | /// The StateTracker that owns this tracking configuration. 30 | /// 31 | public virtual Tracker Tracker { get; } 32 | 33 | /// 34 | /// A dictionary containing the tracked properties. 35 | /// 36 | public Dictionary TrackedProperties { get; } = new Dictionary(); 37 | 38 | /// 39 | /// List containing the events that will trigger persisting 40 | /// 41 | public List PersistTriggers { get; } = new List(); 42 | public Trigger StopTrackingTrigger { get; set; } 43 | 44 | internal TrackingConfiguration() 45 | { 46 | } 47 | 48 | internal TrackingConfiguration( 49 | Tracker tracker, 50 | Type targetType) 51 | { 52 | TargetType = targetType; 53 | Tracker = tracker; 54 | idFunc = target => target.GetType().Name; 55 | 56 | ReadAttributes(); 57 | } 58 | 59 | internal TrackingConfiguration( 60 | TrackingConfiguration baseConfig, 61 | Type targetType) 62 | { 63 | TargetType = targetType; 64 | Tracker = baseConfig.Tracker; 65 | 66 | idFunc = baseConfig.idFunc; 67 | 68 | appliedAction = baseConfig.appliedAction; 69 | persistedAction = baseConfig.persistedAction; 70 | applyingPropertyAction = baseConfig.applyingPropertyAction; 71 | persistingPropertyAction = baseConfig.persistingPropertyAction; 72 | 73 | foreach (var kvp in baseConfig.TrackedProperties) 74 | TrackedProperties.Add(kvp.Key, kvp.Value); 75 | PersistTriggers.AddRange(baseConfig.PersistTriggers); 76 | 77 | ReadAttributes(); 78 | } 79 | 80 | private void ReadAttributes() 81 | { 82 | // todo: use Expression API to generate getters/setters instead of reflection 83 | // [low priority due to low likelyness of 1M+ invocations] 84 | 85 | // todo: add [CanPersist] attribute and use it for canReadFunc 86 | 87 | //set key if [TrackingKey] detected 88 | PropertyInfo keyProperty = TargetType.GetProperties().SingleOrDefault(pi => pi.IsDefined(typeof(TrackingIdAttribute), true)); 89 | if (keyProperty != null) 90 | idFunc = (t) => keyProperty.GetValue(t, null).ToString(); 91 | 92 | //add properties that have [Trackable] applied 93 | foreach (PropertyInfo pi in TargetType.GetProperties()) 94 | { 95 | TrackableAttribute propTrackableAtt = pi.GetCustomAttributes(true).OfType().SingleOrDefault(); 96 | if (propTrackableAtt != null) 97 | { 98 | //use [DefaultValue] if present 99 | DefaultValueAttribute defaultAtt = pi.GetCustomAttribute(); 100 | if (defaultAtt != null) 101 | TrackedProperties[pi.Name] = new TrackedPropertyInfo(x => pi.GetValue(x), (x, v) => SetValue(x, pi, v), defaultAtt.Value); 102 | else 103 | TrackedProperties[pi.Name] = new TrackedPropertyInfo(x => pi.GetValue(x), (x, v) => SetValue(x, pi, v)); 104 | } 105 | } 106 | 107 | foreach (EventInfo eventInfo in TargetType.GetEvents()) 108 | { 109 | var attributes = eventInfo.GetCustomAttributes(true); 110 | 111 | if (attributes.OfType().Any()) 112 | PersistOn(eventInfo.Name); 113 | 114 | if (attributes.OfType().Any()) 115 | StopTrackingOn(eventInfo.Name); 116 | } 117 | } 118 | 119 | private void SetValue(object target, PropertyInfo pi, object value) 120 | { 121 | var valueToWrite = Convert(value, pi.Name, pi.PropertyType); 122 | pi.SetValue(target, valueToWrite); 123 | } 124 | 125 | private static object Convert(object value, string propertyName, Type t) 126 | { 127 | if (value == null) 128 | { 129 | if (t.IsValueType) 130 | throw new ArgumentException($"Cannot write null into non-nullable property {propertyName}"); 131 | } 132 | else 133 | { 134 | var typeOfValue = value.GetType(); 135 | 136 | // This can happen if we're trying to write an Int64 to an Int32 property (in case of overflow it will throw). 137 | // Also can happen for enums. 138 | if (typeOfValue != t && !t.IsAssignableFrom(typeOfValue)) 139 | { 140 | var converter = TypeDescriptor.GetConverter(t); 141 | if (converter.CanConvertFrom(typeOfValue)) 142 | return converter.ConvertFrom(value); 143 | else 144 | { 145 | if (t.IsEnum) 146 | return Enum.ToObject(t, value); 147 | else 148 | return System.Convert.ChangeType(value, t); 149 | } 150 | } 151 | } 152 | 153 | return value; 154 | } 155 | 156 | #region apply/persist events 157 | private bool OnApplyingProperty(object target, string property, ref object value) 158 | { 159 | var args = new PropertyOperationData(property, value); 160 | applyingPropertyAction?.Invoke(target, args); 161 | value = args.Value; 162 | return !args.Cancel; 163 | } 164 | 165 | /// 166 | /// Allows value conversion and cancallation when applying a stored value to a property. 167 | /// 168 | /// 169 | /// 170 | public ITrackingConfiguration WhenApplyingProperty(Action action) 171 | { 172 | applyingPropertyAction = action; 173 | return this; 174 | } 175 | 176 | private void OnStateApplied(object target) 177 | { 178 | appliedAction?.Invoke(target); 179 | } 180 | 181 | /// 182 | /// Allows supplying a callback that will be called when all saved state is applied to a target object. 183 | /// 184 | /// 185 | /// 186 | public ITrackingConfiguration WhenAppliedState(Action action) 187 | { 188 | appliedAction = action; 189 | return this; 190 | } 191 | 192 | private bool OnPersistingProperty(object target, string property, ref object value) 193 | { 194 | var args = new PropertyOperationData(property, value); 195 | persistingPropertyAction?.Invoke(target, args); 196 | value = args.Value; 197 | return !args.Cancel; 198 | } 199 | 200 | /// 201 | /// Allows value conversion and cancallation when persisting a property of the target object. 202 | /// 203 | /// 204 | /// 205 | public ITrackingConfiguration WhenPersistingProperty(Action action) 206 | { 207 | persistingPropertyAction = action; 208 | return this; 209 | } 210 | 211 | private void OnStatePersisted(object target) 212 | { 213 | persistedAction?.Invoke(target); 214 | } 215 | 216 | public ITrackingConfiguration WhenPersisted(Action action) 217 | { 218 | persistedAction = obj => action(obj); 219 | return this; 220 | } 221 | #endregion 222 | 223 | /// 224 | /// Reads the data from the tracked properties and saves it to the data store for the tracked object. 225 | /// 226 | internal void Persist(object target) 227 | { 228 | if (canPersistFunc(target)) 229 | { 230 | var name = idFunc(target); 231 | 232 | IDictionary originalValues = null; 233 | var values = new Dictionary(); 234 | foreach (string propertyName in TrackedProperties.Keys) 235 | { 236 | try 237 | { 238 | var value = TrackedProperties[propertyName].Getter(target); 239 | var shouldPersist = OnPersistingProperty(target, propertyName, ref value); 240 | if (shouldPersist) 241 | { 242 | values[propertyName] = value; 243 | } 244 | else 245 | { 246 | // keeping previously stored value in case persist cancelled 247 | originalValues = originalValues ?? Tracker.Store.GetData(name); 248 | values[propertyName] = originalValues[propertyName]; 249 | Trace.WriteLine($"Persisting cancelled, key='{name}', property='{propertyName}'."); 250 | } 251 | } 252 | catch (Exception ex) 253 | { 254 | Trace.WriteLine($"Persisting failed, property key = '{name}', property = {propertyName}, message='{ex.Message}'."); 255 | } 256 | } 257 | 258 | Tracker.Store.SetData(name, values); 259 | 260 | OnStatePersisted(target); 261 | } 262 | } 263 | 264 | public TrackingConfiguration AsGeneric() 265 | => new TrackingConfiguration(this); 266 | 267 | /// 268 | /// Applies any previously stored data to the tracked properties of the target object. 269 | /// 270 | internal void Apply(object target) 271 | { 272 | if (this.TrackedProperties.Count == 0) 273 | return; 274 | 275 | var name = idFunc(target); 276 | var data = Tracker.Store.GetData(name); 277 | 278 | foreach (string propertyName in TrackedProperties.Keys) 279 | { 280 | var descriptor = TrackedProperties[propertyName]; 281 | 282 | if (data?.ContainsKey(propertyName) == true) 283 | { 284 | try 285 | { 286 | object value = data[propertyName]; 287 | var shouldApply = OnApplyingProperty(target, propertyName, ref value); 288 | if (shouldApply) 289 | { 290 | descriptor.Setter(target, value); 291 | } 292 | else 293 | { 294 | Trace.WriteLine($"Persisting cancelled, key='{name}', property='{propertyName}'."); 295 | } 296 | } 297 | catch (Exception ex) 298 | { 299 | Trace.WriteLine($"TRACKING: Applying tracking to property with key='{propertyName}' failed. ExceptionType:'{ex.GetType().Name}', message: '{ex.Message}'!"); 300 | } 301 | } 302 | else if (descriptor.IsDefaultSpecified) 303 | { 304 | descriptor.Setter(target, descriptor.DefaultValue); 305 | } 306 | } 307 | 308 | OnStateApplied(target); 309 | } 310 | 311 | /// 312 | /// Apply specified defaults to the tracked properties of the target object. 313 | /// 314 | internal void ApplyDefaults(object target) 315 | { 316 | if (this.TrackedProperties.Count == 0) 317 | return; 318 | 319 | var name = idFunc(target); 320 | var data = Tracker.Store.GetData(name); 321 | 322 | foreach (string propertyName in TrackedProperties.Keys) 323 | { 324 | var descriptor = TrackedProperties[propertyName]; 325 | 326 | if (descriptor.IsDefaultSpecified) 327 | { 328 | descriptor.Setter(target, descriptor.DefaultValue); 329 | } 330 | } 331 | 332 | OnStateApplied(target); 333 | } 334 | 335 | public string GetStoreId(object target) => idFunc(target); 336 | 337 | 338 | /// 339 | /// 340 | /// The provided function will be used to get an identifier for a target object in order to identify the data that belongs to it. 341 | /// If true, the name of the type will be included in the id. This prevents id clashes with different types. 342 | /// Serves to distinguish objects with the same ids that are used in different contexts. 343 | /// 344 | public ITrackingConfiguration Id(Func idFunc, object @namespace = null, bool includeType = true) 345 | { 346 | this.idFunc = target => 347 | { 348 | StringBuilder idBuilder = new StringBuilder(); 349 | if (includeType) 350 | idBuilder.Append($"[{target.GetType()}]"); 351 | if (@namespace != null) 352 | idBuilder.Append($"{@namespace}."); 353 | idBuilder.Append($"{idFunc(target)}"); 354 | return idBuilder.ToString(); 355 | }; 356 | 357 | return this; 358 | } 359 | 360 | public ITrackingConfiguration CanPersist(Func canPersistFunc) 361 | { 362 | this.canPersistFunc = canPersistFunc; 363 | return this; 364 | } 365 | 366 | 367 | /// 368 | /// Registers the specified event of the target object as a trigger that will cause the target's data to be persisted. 369 | /// 370 | /// 371 | /// For a Window object, "LocationChanged" and/or "SizeChanged" would be appropriate. 372 | /// 373 | /// 374 | /// Automatically persist a target object when it fires the specified name. 375 | /// 376 | /// The names of the events that will cause the target object's data to be persisted. 377 | /// 378 | public ITrackingConfiguration PersistOn(params string[] eventNames) 379 | { 380 | foreach (string eventName in eventNames) 381 | PersistTriggers.Add(new Trigger(eventName, s => s)); 382 | return this; 383 | } 384 | 385 | /// 386 | /// Automatically persist a target object when the specified eventSourceObject fires the specified event. 387 | /// 388 | /// 389 | /// If not provided, 390 | /// 391 | public ITrackingConfiguration PersistOn(string eventName, object eventSourceObject) 392 | { 393 | PersistOn(eventName, target => eventSourceObject); 394 | return this; 395 | } 396 | 397 | /// 398 | /// Automatically persist a target object when the specified eventSourceObject fires the specified event. 399 | /// 400 | /// The name of the event that should trigger persisting stete. 401 | /// 402 | /// 403 | public ITrackingConfiguration PersistOn(string eventName, Func eventSourceGetter) 404 | { 405 | PersistTriggers.Add(new Trigger(eventName, target => eventSourceGetter(target))); 406 | return this; 407 | } 408 | 409 | /// 410 | /// Stop tracking the target when it fires the specified event. 411 | /// 412 | /// 413 | /// 414 | public ITrackingConfiguration StopTrackingOn(string eventName) 415 | { 416 | return StopTrackingOn(eventName, target => target); 417 | } 418 | 419 | /// 420 | /// Stop tracking the target when the specified eventSource object fires the specified event. 421 | /// 422 | /// 423 | /// 424 | /// 425 | public ITrackingConfiguration StopTrackingOn(string eventName, object eventSource) 426 | { 427 | return StopTrackingOn(eventName, target => eventSource); 428 | } 429 | 430 | /// 431 | /// Stop tracking the target when the specified eventSource object fires the specified event. 432 | /// 433 | /// 434 | /// 435 | /// 436 | public ITrackingConfiguration StopTrackingOn(string eventName, Func eventSourceGetter) 437 | { 438 | StopTrackingTrigger = new Trigger(eventName, target => eventSourceGetter(target)); 439 | return this; 440 | } 441 | 442 | internal void StopTracking(object target) 443 | { 444 | // unsubscribe from all trigger events 445 | foreach (var trigger in PersistTriggers) 446 | trigger.Unsubscribe(target); 447 | 448 | // unsubscribe from stoptracking trigger too 449 | StopTrackingTrigger?.Unsubscribe(target); 450 | 451 | Tracker.RemoveFromList(target); 452 | } 453 | 454 | internal void StartTracking(object target) 455 | { 456 | // listen for trigger events (for persisting) 457 | foreach (var trigger in PersistTriggers) 458 | trigger.Subscribe(target, () => Persist(target)); 459 | 460 | // listen to stoptracking event 461 | StopTrackingTrigger?.Subscribe(target, () => StopTracking(target)); 462 | } 463 | 464 | /// 465 | /// Set up tracking for the specified property. Allows supplying a name for the property. 466 | /// This overload is used when the target object has a list of child objects whose properties 467 | /// it wishes to track. Each child object's properties can be tracked with a different name, 468 | /// e.g. by including the index in the name. 469 | /// 470 | /// Type of target object 471 | /// Type of property 472 | /// Name to use when tracking the property's data. 473 | /// The expression that points to the property to track. Supports accessing properties of nested objects. 474 | /// 475 | public ITrackingConfiguration Property(Expression> propertyAccessExpression, string name = null) 476 | { 477 | return Property(name, propertyAccessExpression, false, default(TProperty)); 478 | } 479 | 480 | /// 481 | /// Set up tracking for the specified property. Allows supplying a name for the property. 482 | /// This overload is used when the target object has a list of child objects whose properties 483 | /// it wishes to track. Each child object's properties can be tracked with a different name, 484 | /// e.g. by including the index in the name. 485 | /// 486 | /// Type of target object 487 | /// Type of property 488 | /// Name to use when tracking the property's data. 489 | /// The expression that points to the property to track. Supports accessing properties of nested objects. 490 | /// If there is no value in the store for the property, the defaultValue will be used. 491 | /// 492 | public ITrackingConfiguration Property(Expression> propertyAccessExpression, TProperty defaultValue, string name = null) 493 | { 494 | return Property(name, propertyAccessExpression, true, defaultValue); 495 | } 496 | 497 | internal ITrackingConfiguration Property(string name, Expression> propertyAccessExpression, bool defaultSpecified, TProperty defaultValue) 498 | { 499 | if (name == null && propertyAccessExpression.Body is MemberExpression me) 500 | { 501 | // If not specified, use the entire expression as the name of the property. 502 | // Note: we don't use just the member name because it might conflict with 503 | // another property that uses a different expression but the same member name 504 | // e.g. "firstCol.Width" and "secondCol.Width". 505 | name = me.ToString(); 506 | } 507 | 508 | var membershipExpression = propertyAccessExpression.Body; 509 | var getter = propertyAccessExpression.Compile(); 510 | 511 | var right = Expression.Parameter(typeof(object)); 512 | var propType = membershipExpression.Type; 513 | var setter = Expression.Lambda(Expression.Block(Expression.Assign(membershipExpression, Expression.Convert(right, membershipExpression.Type)), Expression.Empty()), propertyAccessExpression.Parameters[0], right).Compile() as Action; 514 | if (defaultSpecified) 515 | TrackedProperties[name] = new TrackedPropertyInfo(x => getter((T)x), (x, v) => setter((T)x, v), defaultValue); 516 | else 517 | TrackedProperties[name] = new TrackedPropertyInfo(x => getter((T)x), (x, v) => setter((T)x, v)); 518 | return this; 519 | } 520 | 521 | /// 522 | /// Set up tracking for one or more properties. The expression should be an anonymous type projection (e.g. x => new { x.MyProp1, x.MyProp2 }). 523 | /// 524 | /// Type of target object 525 | /// A projection of properties to track. Allows providing nested object properties. 526 | /// 527 | public ITrackingConfiguration Properties(Expression> projection) 528 | { 529 | NewExpression newExp = projection.Body as NewExpression; 530 | 531 | // VB.NET encapsulates the new expression in a convert-to-object expression 532 | if (newExp == null && projection.Body is UnaryExpression ue && ue.NodeType == ExpressionType.Convert && ue.Type == typeof(object)) 533 | newExp = ue.Operand as NewExpression; 534 | 535 | if (newExp != null) 536 | { 537 | var accessors = newExp.Members.Select((m, i) => 538 | { 539 | var right = Expression.Parameter(typeof(object)); 540 | var propType = (m as PropertyInfo).PropertyType; 541 | return new 542 | { 543 | name = m.Name, 544 | type = propType, 545 | getter = (Expression.Lambda(Expression.Convert(newExp.Arguments[i] as MemberExpression, typeof(object)), projection.Parameters[0]).Compile() as Func), 546 | // todo: call the Convert method instead of using Expression.Convert which will not work for enums 547 | setter = Expression.Lambda(Expression.Block(Expression.Assign(newExp.Arguments[i], Expression.Convert(right, propType)), Expression.Empty()), projection.Parameters[0], right).Compile() as Action 548 | }; 549 | }); 550 | 551 | foreach (var a in accessors) 552 | { 553 | TrackedProperties[a.name] = new TrackedPropertyInfo(x => a.getter((T)x), (x, v) => a.setter((T)x, Convert(v, a.name, a.type))); 554 | } 555 | } 556 | else 557 | { 558 | throw new ArgumentException("Expression must project properties as an anonymous class e.g. f => new { f.Height, f.Width } or access a single property e.g. f => f.Text."); 559 | } 560 | return this; 561 | } 562 | } 563 | } 564 | -------------------------------------------------------------------------------- /Jot/Configuration/TrackingConfigurationGeneric.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace Jot.Configuration 6 | { 7 | /// 8 | /// A TrackingConfiguration determines how a target object will be tracked. 9 | /// This includes list of properties to track, persist triggers and id getter. 10 | /// 11 | /// 12 | /// Derives from TrackingConfiguration and adds a generic strongly typed API for configuring tracking. 13 | /// This class does not provide any new functionality nor store any additional state.All calls are forwarded to the base class. 14 | /// 15 | public sealed class TrackingConfiguration : ITrackingConfiguration 16 | { 17 | private readonly TrackingConfiguration inner; 18 | 19 | public Tracker Tracker => inner.Tracker; 20 | 21 | public List PersistTriggers => inner.PersistTriggers; 22 | 23 | public Trigger StopTrackingTrigger { get => inner.StopTrackingTrigger; set => inner.StopTrackingTrigger = value; } 24 | 25 | public Type TargetType => inner.TargetType; 26 | 27 | public Dictionary TrackedProperties => inner.TrackedProperties; 28 | 29 | internal TrackingConfiguration(TrackingConfiguration inner) 30 | { 31 | this.inner = inner; 32 | } 33 | 34 | /// 35 | /// Start tracking the target object. This will apply any previously stored data and start 36 | /// listening for events that indicate persisting new data is required. 37 | /// 38 | /// The target object to track. 39 | public void Track(T target) 40 | => inner.Tracker.Track(target, inner); 41 | 42 | /// 43 | /// Allows value conversion and cancellation when applying a stored value to a property. 44 | /// 45 | /// 46 | /// 47 | public TrackingConfiguration WhenApplyingProperty(Action action) 48 | { 49 | inner.WhenApplyingProperty((obj, prop) => action((T)obj, prop)); 50 | return this; 51 | } 52 | 53 | /// 54 | /// Allows supplying a callback that will be called when all saved state is applied to a target object. 55 | /// 56 | /// 57 | /// 58 | public TrackingConfiguration WhenAppliedState(Action action) 59 | { 60 | inner.WhenAppliedState(obj => action((T)obj)); 61 | return this; 62 | } 63 | 64 | /// 65 | /// Allows value conversion and cancellation when persisting a property of the target object. 66 | /// 67 | /// 68 | /// 69 | public TrackingConfiguration WhenPersistingProperty(Action action) 70 | { 71 | inner.WhenPersistingProperty((obj, prop) => action((T)obj, prop)); 72 | return this; 73 | } 74 | 75 | public TrackingConfiguration WhenPersisted(Action action) 76 | { 77 | inner.WhenPersisted(obj => action((T)obj)); 78 | return this; 79 | } 80 | 81 | /// 82 | /// 83 | /// The provided function will be used to get an identifier for a target object in order to identify the data that belongs to it. 84 | /// If true, the name of the type will be included in the id. This prevents id clashes with different types. 85 | /// Serves to distinguish objects with the same ids that are used in different contexts. 86 | /// 87 | public TrackingConfiguration Id(Func idFunc, object @namespace = null, bool includeType = true) 88 | { 89 | inner.Id(t => idFunc((T)t), @namespace, includeType); 90 | return this; 91 | } 92 | 93 | /// 94 | /// 95 | /// The provided function will be used to get an identifier for a target object in order to identify the data that belongs to it. 96 | /// 97 | public TrackingConfiguration CanPersist(Func canPersistFunc) 98 | { 99 | inner.CanPersist(t => canPersistFunc((T)t)); 100 | return this; 101 | } 102 | 103 | /// 104 | /// Registers the specified event of the target object as a trigger that will cause the target's data to be persisted. 105 | /// 106 | /// 107 | /// For a Window object, "LocationChanged" and/or "SizeChanged" would be appropriate. 108 | /// 109 | /// 110 | /// Automatically persist a target object when it fires the specified name. 111 | /// 112 | /// The names of the events that will cause the target object's data to be persisted. 113 | /// 114 | public TrackingConfiguration PersistOn(params string[] eventNames) 115 | { 116 | inner.PersistOn(eventNames); 117 | return this; 118 | } 119 | 120 | /// 121 | /// Automatically persist a target object when the specified eventSourceObject fires the specified event. 122 | /// 123 | /// 124 | /// If not provided, 125 | /// 126 | public TrackingConfiguration PersistOn(string eventName, object eventSourceObject) 127 | { 128 | inner.PersistOn(eventName, eventSourceObject); 129 | return this; 130 | } 131 | 132 | /// 133 | /// Automatically persist a target object when the specified eventSourceObject fires the specified event. 134 | /// 135 | /// The name of the event that should trigger persisting stete. 136 | /// 137 | /// 138 | public TrackingConfiguration PersistOn(string eventName, Func eventSourceGetter) 139 | { 140 | inner.PersistOn(eventName, t => eventSourceGetter((T)t)); 141 | return this; 142 | } 143 | 144 | /// 145 | /// Stop tracking the target when it fires the specified event. 146 | /// 147 | /// 148 | /// 149 | public TrackingConfiguration StopTrackingOn(string eventName) 150 | { 151 | inner.StopTrackingOn(eventName); 152 | return this; 153 | } 154 | 155 | /// 156 | /// Stop tracking the target when the specified eventSource object fires the specified event. 157 | /// 158 | /// 159 | /// 160 | /// 161 | public TrackingConfiguration StopTrackingOn(string eventName, object eventSource) 162 | { 163 | inner.StopTrackingOn(eventName, eventSource); 164 | return this; 165 | } 166 | 167 | /// 168 | /// Stop tracking the target when the specified eventSource object fires the specified event. 169 | /// 170 | /// 171 | /// 172 | /// 173 | public TrackingConfiguration StopTrackingOn(string eventName, Func eventSourceGetter) 174 | { 175 | inner.StopTrackingOn(eventName, t => eventSourceGetter((T)t)); 176 | return this; 177 | } 178 | 179 | /// 180 | /// Set up tracking for the specified property. 181 | /// 182 | /// 183 | /// 184 | /// 185 | /// 186 | public TrackingConfiguration Property(Expression> propertyAccessExpression, string name = null) 187 | { 188 | return Property(name, propertyAccessExpression, false, default(K)); 189 | } 190 | 191 | /// 192 | /// Set up tracking for the specified property. 193 | /// 194 | /// 195 | /// The name of the property in the store 196 | /// The expression that points to the specified property. Can navigate multiple levels. 197 | /// If there is no value in the store for the property, the defaultValue will be used. 198 | /// 199 | public TrackingConfiguration Property(Expression> propertyAccessExpression, TProperty defaultValue, string name = null) 200 | { 201 | return Property(name, propertyAccessExpression, true, defaultValue); 202 | } 203 | 204 | private TrackingConfiguration Property(string name, Expression> propertyAccessExpression, bool defaultSpecified, TProperty defaultValue) 205 | { 206 | inner.Property(name, propertyAccessExpression, defaultSpecified, defaultValue); 207 | return this; 208 | } 209 | 210 | /// 211 | /// Set up tracking for one or more properties. 212 | /// 213 | /// Describes which properties of the target object to track by returning an anonymous type projection (e.g. x => new { x.MyProp1, x.MyProp2 }) 214 | /// 215 | public TrackingConfiguration Properties(Expression> projection) 216 | { 217 | inner.Properties(projection); 218 | return this; 219 | } 220 | 221 | public ITrackingConfiguration CanPersist(Func canPersistFunc) 222 | => inner.CanPersist(canPersistFunc); 223 | 224 | public string GetStoreId(object target) 225 | => inner.GetStoreId(target); 226 | 227 | ITrackingConfiguration ITrackingConfiguration.Id(Func idFunc, object @namespace, bool includeType) 228 | { 229 | inner.Id(idFunc, @namespace, includeType); 230 | return this; 231 | } 232 | 233 | ITrackingConfiguration ITrackingConfiguration.PersistOn(params string[] eventNames) 234 | { 235 | inner.PersistOn(eventNames); 236 | return this; 237 | } 238 | 239 | ITrackingConfiguration ITrackingConfiguration.PersistOn(string eventName, Func eventSourceGetter) 240 | { 241 | inner.PersistOn(eventName, eventSourceGetter); 242 | return this; 243 | } 244 | 245 | ITrackingConfiguration ITrackingConfiguration.PersistOn(string eventName, object eventSourceObject) 246 | { 247 | inner.PersistOn(eventName, eventSourceObject); 248 | return this; 249 | } 250 | 251 | ITrackingConfiguration ITrackingConfiguration.StopTrackingOn(string eventName) 252 | { 253 | inner.StopTrackingOn(eventName); 254 | return this; 255 | } 256 | 257 | ITrackingConfiguration ITrackingConfiguration.StopTrackingOn(string eventName, Func eventSourceGetter) 258 | { 259 | inner.StopTrackingOn(eventName, eventSourceGetter); 260 | return this; 261 | } 262 | 263 | ITrackingConfiguration ITrackingConfiguration.StopTrackingOn(string eventName, object eventSource) 264 | { 265 | inner.StopTrackingOn(eventName, eventSource); 266 | return this; 267 | } 268 | 269 | ITrackingConfiguration ITrackingConfiguration.WhenAppliedState(Action action) 270 | { 271 | inner.WhenAppliedState(action); 272 | return this; 273 | } 274 | 275 | ITrackingConfiguration ITrackingConfiguration.WhenApplyingProperty(Action action) 276 | { 277 | inner.WhenApplyingProperty(action); 278 | return this; 279 | } 280 | 281 | ITrackingConfiguration ITrackingConfiguration.WhenPersisted(Action action) 282 | { 283 | inner.WhenPersisted(action); 284 | return this; 285 | } 286 | 287 | ITrackingConfiguration ITrackingConfiguration.WhenPersistingProperty(Action action) 288 | { 289 | inner.WhenPersistingProperty(action); 290 | return this; 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Jot/Configuration/TrackingOperationEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Jot.Configuration 4 | { 5 | /// 6 | /// Event args for a tracking operation. Enables the handler to cancel the operation and modify the data that will be persisted/applied. 7 | /// 8 | public class PropertyOperationData 9 | { 10 | public bool Cancel { get; set; } 11 | 12 | /// 13 | /// The property that is being persisted or applied to. 14 | /// 15 | public string Property { get; } 16 | 17 | /// 18 | /// The value that is being persited or applied. Has a setter to support converting/mapping/limiting values when applying/persisting. 19 | /// 20 | public object Value { get; set; } 21 | 22 | /// 23 | /// Creates a new instance of PropertyData. 24 | /// 25 | /// The property that is being persisted or applied to. 26 | /// The value that is being persited or applied. 27 | public PropertyOperationData(string property, object value) 28 | { 29 | Property = property; 30 | Value = value; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Jot/Configuration/Trigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace Jot.Configuration 8 | { 9 | public class Trigger 10 | { 11 | readonly ConditionalWeakTable _handlers = new ConditionalWeakTable(); 12 | 13 | public string EventName { get; } 14 | public Func SourceGetter { get; } 15 | 16 | public Trigger(string eventName, Func sourceGetter) 17 | { 18 | EventName = eventName; 19 | SourceGetter = sourceGetter; 20 | } 21 | 22 | public void Subscribe(object target, Action action) 23 | { 24 | // clear a possible previous subscription for the same target/event 25 | Unsubscribe(target); 26 | 27 | var source = SourceGetter(target); 28 | 29 | EventInfo eventInfo = source.GetType().GetEvent(EventName); 30 | 31 | if (eventInfo == null) 32 | throw new ArgumentException($"Event '{EventName}' not found on target of type '{source.GetType().Name}'. Check the tracking configuration for this type."); 33 | 34 | var parameters = eventInfo.EventHandlerType 35 | .GetMethod("Invoke") 36 | .GetParameters() 37 | .Select(parameter => Expression.Parameter(parameter.ParameterType)) 38 | .ToArray(); 39 | 40 | var handler = Expression.Lambda( 41 | eventInfo.EventHandlerType, 42 | Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), 43 | parameters) 44 | .Compile(); 45 | 46 | eventInfo.AddEventHandler(source, handler); 47 | 48 | _handlers.Add(target, handler); 49 | } 50 | 51 | public void Unsubscribe(object target) 52 | { 53 | if (_handlers.TryGetValue(target, out Delegate handler)) 54 | { 55 | var source = SourceGetter(target); 56 | EventInfo eventInfo = source.GetType().GetEvent(EventName); 57 | eventInfo.RemoveEventHandler(source, handler); 58 | _handlers.Remove(target); 59 | } 60 | } 61 | 62 | internal void Subscribe(T target, object p) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Jot/Jot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 2.1.17 6 | true 7 | Antonio Nakic-Alfirevic 8 | Windy Range Software 9 | true 10 | false 11 | Jot is a library for tracking application state. Typically this includes window sizes and locations, last entered data, application settings, and user preferences. In short: a better alternative to using settings files. 12 | Windy Range Software d.o.o. 13 | https://github.com/anakic/Jot 14 | https://github.com/anakic/Jot 15 | .net C# dotnet netstandard dotnetcore app settings config state persistence 16 | mykey.snk 17 | MIT 18 | Added missing XML docs 19 | README.md 20 | 21 | 22 | 23 | true 24 | 25 | 0 26 | full 27 | true 28 | 29 | 30 | 31 | 5 32 | 33 | 34 | 35 | Jot.xml 36 | 37 | 38 | 39 | C:\work\projects\querystorm\Jot\Jot\Jot.xml 40 | 41 | 42 | 43 | 44 | True 45 | \ 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Jot/Jot.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /Jot/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Jot": { 4 | "commandName": "Project", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Jot/Storage/IStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Jot.Storage 4 | { 5 | public interface IStore 6 | { 7 | IEnumerable ListIds(); 8 | void SetData(string id, IDictionary values); 9 | IDictionary GetData(string id); 10 | void ClearData(string id); 11 | void ClearAll(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Jot/Storage/JsonFileStore.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Reflection; 9 | 10 | namespace Jot.Storage 11 | { 12 | /// 13 | /// An implementation of IStore that saves data to a json file. 14 | /// 15 | public class JsonFileStore : IStore 16 | { 17 | #region custom serialization (for object type handling) 18 | private class IPAddressConverter : JsonConverter 19 | { 20 | public override bool CanConvert(Type objectType) 21 | { 22 | return objectType == typeof(IPAddress); 23 | } 24 | 25 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 26 | { 27 | var value = reader.Value; 28 | if (value != null) 29 | return IPAddress.Parse((string)reader.Value); 30 | else 31 | return null; 32 | } 33 | 34 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 35 | { 36 | writer.WriteValue(value.ToString()); 37 | } 38 | } 39 | 40 | private class StoreItemConverter : JsonConverter 41 | { 42 | public override bool CanConvert(Type objectType) 43 | { 44 | return objectType == typeof(StoreItem); 45 | } 46 | 47 | public override bool CanRead 48 | { 49 | get { return true; } 50 | } 51 | 52 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 53 | { 54 | reader.Read();//read "Type" attribute name 55 | reader.Read();//read "Type" attribute value 56 | Type t = serializer.Deserialize(reader); 57 | 58 | var x = reader.Read();//read "Name" attribute name 59 | var name = reader.ReadAsString();//read "Name" attribute value 60 | 61 | reader.Read();//read "Value" attribute name 62 | reader.Read();//read "value" attribute value 63 | var res = serializer.Deserialize(reader, t); 64 | 65 | reader.Read();//position to next item 66 | 67 | return new StoreItem() { Name = name, Type = t, Value = res }; 68 | } 69 | 70 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 71 | { 72 | //nothing fancy, standard serialization 73 | var converters = serializer.Converters.ToArray(); 74 | var jObject = JObject.FromObject(value); 75 | jObject.WriteTo(writer, converters); 76 | } 77 | } 78 | 79 | private class StoreItem 80 | { 81 | [JsonProperty(Order = 1)] 82 | public Type Type { get; set; } 83 | [JsonProperty(Order = 2)] 84 | public string Name { get; set; } 85 | [JsonProperty(Order = 3)] 86 | public object Value { get; set; } 87 | } 88 | #endregion 89 | 90 | /// 91 | /// The folder in which the store files will be located. 92 | /// 93 | public string FolderPath { get; set; } 94 | 95 | /// 96 | /// Creates a JsonFileStore that will store files in a per-user folder (%appdata%\[companyname]\[productname]). 97 | /// 98 | /// 99 | /// CompanyName and ProductName are read from the entry assembly's attributes. 100 | /// 101 | public JsonFileStore() 102 | : this(true) 103 | { 104 | } 105 | 106 | /// 107 | /// Creates a JsonFileStore that will store files in a per-user or per-machine folder. (%appdata% or %allusersprofile% + \[companyname]\[productname]). 108 | /// 109 | /// Specified if a per-user or per-machine folder will be used for storing the data. 110 | /// 111 | /// CompanyName and ProductName are read from the entry assembly's attributes. 112 | /// 113 | public JsonFileStore(bool perUser) 114 | : this(ConstructPath(perUser ? Environment.SpecialFolder.ApplicationData : Environment.SpecialFolder.CommonApplicationData)) 115 | { 116 | } 117 | 118 | /// 119 | /// Creates a JsonFileStore that will store files in the specified folder. 120 | /// 121 | /// The folder inside which the json files for tracked objects will be stored. 122 | public JsonFileStore(Environment.SpecialFolder folder) 123 | : this(ConstructPath(folder)) 124 | { 125 | } 126 | 127 | /// 128 | /// Creates a JsonFileStore that will store files in the specified folder. 129 | /// 130 | /// The folder inside which the json files for tracked objects will be stored. 131 | public JsonFileStore(string storeFolderPath) 132 | { 133 | FolderPath = storeFolderPath; 134 | } 135 | 136 | /// 137 | /// Loads values from the json file into a dictionary. 138 | /// 139 | /// 140 | public IDictionary GetData(string id) 141 | { 142 | string filePath = GetfilePath(id); 143 | List storeItems = null; 144 | if (File.Exists(filePath)) 145 | { 146 | try 147 | { 148 | var fileContents = File.ReadAllText(filePath); 149 | storeItems = JsonConvert.DeserializeObject>(fileContents, new StoreItemConverter(), new IPAddressConverter()); 150 | } 151 | catch { } 152 | } 153 | 154 | if (storeItems == null) 155 | storeItems = new List(); 156 | 157 | return storeItems.ToDictionary(item => item.Name, item => item.Value); 158 | } 159 | 160 | /// 161 | /// Stores the values as a json file. 162 | /// 163 | /// 164 | /// 165 | public void SetData(string id, IDictionary values) 166 | { 167 | string filePath = GetfilePath(id); 168 | var list = values.Select(kvp => new StoreItem() { Name = kvp.Key, Value = kvp.Value, Type = kvp.Value?.GetType() }); 169 | string serialized = JsonConvert.SerializeObject(list, new JsonSerializerSettings() { Formatting = Formatting.Indented, TypeNameHandling=TypeNameHandling.None, Converters = new JsonConverter[] { new IPAddressConverter() } }); 170 | 171 | string directory = Path.GetDirectoryName(filePath); 172 | if (!Directory.Exists(directory)) 173 | Directory.CreateDirectory(directory); 174 | 175 | File.WriteAllText(filePath, serialized); 176 | } 177 | 178 | private string GetfilePath(string id) 179 | { 180 | return Path.Combine(FolderPath, $"{id}.json"); 181 | } 182 | 183 | private static string ConstructPath(Environment.SpecialFolder baseFolder) 184 | { 185 | string companyPart = string.Empty; 186 | string appNamePart = string.Empty; 187 | 188 | Assembly entryAssembly = Assembly.GetEntryAssembly(); 189 | if (entryAssembly != null)//for unit tests entryAssembly == null 190 | { 191 | AssemblyCompanyAttribute companyAttribute = (AssemblyCompanyAttribute)Attribute.GetCustomAttribute(entryAssembly, typeof(AssemblyCompanyAttribute)); 192 | if (!string.IsNullOrEmpty(companyAttribute.Company)) 193 | companyPart = $"{companyAttribute.Company}\\"; 194 | AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)Attribute.GetCustomAttribute(entryAssembly, typeof(AssemblyTitleAttribute)); 195 | if (!string.IsNullOrEmpty(titleAttribute.Title)) 196 | appNamePart = $"{titleAttribute.Title}\\"; 197 | } 198 | 199 | return Path.Combine(Environment.GetFolderPath(baseFolder), $@"{companyPart}{appNamePart}"); 200 | } 201 | 202 | public IEnumerable ListIds() 203 | { 204 | return Directory.GetFiles(FolderPath, "*.json").Select(Path.GetFileNameWithoutExtension); 205 | } 206 | 207 | public void ClearData(string id) 208 | { 209 | File.Delete(GetfilePath(id)); 210 | } 211 | 212 | public void ClearAll() 213 | { 214 | foreach (var id in ListIds()) 215 | ClearData(id); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Jot/Tracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Jot.Storage; 5 | using System.Runtime.CompilerServices; 6 | using Jot.Configuration; 7 | using System.Reflection; 8 | using System.Globalization; 9 | 10 | namespace Jot 11 | { 12 | /// 13 | /// A StateTracker is an object responsible for tracking the specified properties of the specified target objects. 14 | /// Tracking means persisting the values of the specified object properties, and restoring this data when appropriate. 15 | /// 16 | public class Tracker 17 | { 18 | // configurations for types 19 | readonly Dictionary _typeConfigurations = new Dictionary(); 20 | 21 | // Weak reference dictionary 22 | readonly ConditionalWeakTable _configurationsDict = new ConditionalWeakTable(); 23 | 24 | // Workaround: 25 | // ConditionalWeakTable does not support getting a list of all keys, which we need for a global persist 26 | readonly List _trackedObjects = new List(); 27 | 28 | /// 29 | /// The object that is used to store and retrieve tracked data. 30 | /// 31 | public IStore Store { get; set; } 32 | 33 | /// 34 | /// Creates a StateTracker that uses json files in a per-user folder to store the data. 35 | /// 36 | public Tracker() 37 | : this(new JsonFileStore()) 38 | { 39 | } 40 | 41 | /// 42 | /// Creates a new instance of the state tracker with the specified storage. 43 | /// 44 | /// The factory that will create an IStore for each tracked object's data. 45 | public Tracker( 46 | IStore store) 47 | { 48 | Store = store; 49 | } 50 | 51 | // todo: allow caller to configure via action argument 52 | 53 | /// 54 | /// Track a target object. This will apply any previously stored state to the target and 55 | /// start listening for events that indicate persisting new data is required. 56 | /// 57 | /// 58 | public void Track(object target) 59 | { 60 | Track(target, Configure(target)); 61 | } 62 | 63 | // this is internal to allow TrackingConfiguration to call it so 64 | // we can avoid the extra lookup (finding the configuration) 65 | internal void Track(object target, TrackingConfiguration config) 66 | { 67 | // apply any previously stored data 68 | config.Apply(target); 69 | 70 | // listen for persist requests 71 | config.StartTracking(target); 72 | 73 | // add to list of objects to track 74 | _trackedObjects.Add(new WeakReference(target)); 75 | } 76 | 77 | /// 78 | /// Apply any previously stored data to the target object. 79 | /// 80 | /// 81 | public void Apply(object target) 82 | { 83 | this.Configure(target) 84 | .Apply(target); 85 | } 86 | 87 | /// 88 | /// Apply specified defaults to the tracked properties of the target object. 89 | /// 90 | public void ApplyDefaults(object target) 91 | { 92 | this.Configure(target) 93 | .ApplyDefaults(target); 94 | } 95 | 96 | /// 97 | /// Forget any saved state for the object with the specified id. 98 | /// 99 | public void Forget(string id) 100 | { 101 | Store.ClearData(id); 102 | } 103 | 104 | /// 105 | /// Forget any saved state for the target object. 106 | /// 107 | public void Forget(object target) 108 | { 109 | var id = this.Configure(target).GetStoreId(target); 110 | Forget(id); 111 | } 112 | 113 | /// 114 | /// Forget all saved state. 115 | /// 116 | public void ForgetAll() 117 | { 118 | Store.ClearAll(); 119 | } 120 | 121 | /// 122 | /// Gets or creates a tracking configuration for the target object. 123 | /// 124 | public TrackingConfiguration Configure(object target) 125 | { 126 | TrackingConfiguration config; 127 | if (_configurationsDict.TryGetValue(target, out TrackingConfiguration cfg)) 128 | config = cfg; 129 | else 130 | { 131 | config = Configure(target.GetType()); 132 | 133 | // if the object or the caller want to customize the config for this type, copy the config so they don't mess with the config for the type 134 | if (target is ITrackingAware) 135 | { 136 | config = new TrackingConfiguration(config, target.GetType()); 137 | 138 | // allow the object to adjust the configuration 139 | if (target is ITrackingAware ita) 140 | ita.ConfigureTracking(config); 141 | } 142 | 143 | _configurationsDict.Add(target, config); 144 | } 145 | return config; 146 | } 147 | 148 | /// 149 | /// Gets or creates a tracking configuration for the specified type. Objects of the 150 | /// specified type will be tracked according to the settings that are defined in the 151 | /// configuration object. 152 | /// 153 | public TrackingConfiguration Configure() 154 | { 155 | return new TrackingConfiguration(Configure(typeof(T))); 156 | } 157 | 158 | /// 159 | /// Gets or creates a tracking configuration for the specified type. Objects of the 160 | /// specified type will be tracked according to the settings that are defined in the 161 | /// configuration object. 162 | /// 163 | public TrackingConfiguration Configure(Type t) 164 | { 165 | TrackingConfiguration configuration; 166 | if (_typeConfigurations.TryGetValue(t, out var typeConfiguration)) 167 | { 168 | // if a config for this exact type exists return it 169 | configuration = typeConfiguration; 170 | } 171 | else 172 | { 173 | // todo: we should make a config for each base type recursively, in case at a later point we add config for a base type 174 | // tbd : should configurations delegate work to base classes, rather than copying their config data? 175 | // if a config for this exact type does not exist, copy from base type's config or create a blank one 176 | var baseConfig = FindConfiguration(t); 177 | if (baseConfig != null) 178 | configuration = new TrackingConfiguration(baseConfig, t); 179 | else 180 | configuration = new TrackingConfiguration(this, t); 181 | _typeConfigurations[t] = configuration; 182 | } 183 | return configuration; 184 | } 185 | 186 | private TrackingConfiguration FindConfiguration(Type type) 187 | { 188 | if (_typeConfigurations.TryGetValue(type, out var config)) 189 | return config; 190 | else 191 | { 192 | if (type == typeof(object) || type.BaseType == null) 193 | return null; 194 | else 195 | return FindConfiguration(type.BaseType); 196 | } 197 | } 198 | 199 | /// 200 | /// Stop tracking the target object. This prevents the persisting 201 | /// the target's properties when PersistAll is called on the tracker. 202 | /// It is used to prevent saving invalid data when the target object 203 | /// still exists but is in an invalid state (e.g. disposed forms). 204 | /// 205 | public void StopTracking(object target) 206 | { 207 | if (_configurationsDict.TryGetValue(target, out TrackingConfiguration cfg)) 208 | { 209 | cfg.StopTracking(target); 210 | } 211 | } 212 | 213 | // allows the tracking configuration to remove an object from the lists (so that it's not hit by global persist) 214 | internal void RemoveFromList(object target) 215 | { 216 | _configurationsDict.Remove(target); 217 | _trackedObjects.RemoveAll(t => t.Target == target); 218 | } 219 | 220 | /// 221 | /// Persists the tracked properties of the target object. 222 | /// 223 | /// 224 | public void Persist(object target) 225 | { 226 | Configure(target).Persist(target); 227 | } 228 | 229 | /// 230 | /// Runs a global persist for all objects that are still alive and tracked. Waits for finalizers to complete first. 231 | /// 232 | public void PersistAll() 233 | { 234 | GC.WaitForPendingFinalizers(); 235 | 236 | foreach (var target in _trackedObjects.Where(o => o.IsAlive).Select(o => o.Target)) 237 | { 238 | if (_configurationsDict.TryGetValue(target, out var configuration)) 239 | configuration.Persist(target); 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Jot/mykey.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anakic/Jot/5cdab283d4c7db1988889d9eecff9c3451e4d018/Jot/mykey.snk -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jot - a .NET library for state persistence 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/3p31q9b15v46sudk/branch/master?svg=true)](https://ci.appveyor.com/project/anakic/jot/branch/master) 4 | 5 | ## Introduction 6 | Almost every application needs to keep track of its own state, regardless of what it otherwise does. This typically includes: 7 | 8 | 1. Sizes and locations of movable/resizable elements of the UI (forms, tool windows, draggable toolbars...) 9 | 1. Last entered data (e.g. username, selected tab indexes, recently opened files...) 10 | 1. Settings and user preferences 11 | 12 | A common approach is to store this data in a .settings file and read and update it as needed. This involves writing a lot of boilerplate code to copy that data back and forth. This code is generally tedious, error-prone and no fun to write. 13 | 14 | With Jot, you only need to declare which properties of which objects you want to track, and when to persist and apply data. This is a better abstraction for this requirement, resulting in more readable and concise code. 15 | 16 | ## Installation 17 | 18 | Jot is available on NuGet and can be installed from the package manager console: 19 | 20 | `install-package Jot` 21 | 22 | 23 | ## Example: Persisting the size and location of a Window 24 | To illustrate the basic idea, let's compare two ways of dealing with this requirement: .settings file (Scenario A) versus Jot (Scenario B). 25 | 26 | ### Scenario A (.settings file) 27 | 28 | **Step 1:** Define settings 29 | 30 | ![](http://www.codeproject.com/KB/cs/475498/settings.jpg) 31 | 32 | **Step 2:** Apply previously stored data 33 | 34 | ``` C# 35 | public MainWindow() 36 | { 37 | InitializeComponent(); 38 | 39 | this.Left = MySettings.Default.MainWindowLeft; 40 | this.Top = MySettings.Default.MainWindowTop; 41 | this.Width = MySettings.Default.MainWindowWidth; 42 | this.Height = MySettings.Default.MainWindowHeight; 43 | this.WindowState = MySettings.Default.MainWindowWindowState; 44 | } 45 | ``` 46 | 47 | **Step 3:** Persist updated data before the window is closed 48 | 49 | ``` C# 50 | protected override void OnClosed(EventArgs e) 51 | { 52 | MySettings.Default.MainWindowLeft = this.Left; 53 | MySettings.Default.MainWindowTop = this.Top; 54 | MySettings.Default.MainWindowWidth = this.Width; 55 | MySettings.Default.MainWindowHeight = this.Height; 56 | MySettings.Default.MainWindowWindowState = this.WindowState; 57 | 58 | MySettings.Default.Save(); 59 | 60 | base.OnClosed(e); 61 | } 62 | ``` 63 | 64 | This is a lot of work, even for a single window. If there were 10 resizable/movable elements of the UI, the settings file would become a jungle of similarly named properties, making this code quite tedious and error prone to write. 65 | 66 | Also notice that for each property of the window, we need to mention it in five places (in the settings file, twice in the constructor and twice in OnClosed). 67 | 68 | 69 | ### Scenario B (Jot) 70 | 71 | **Step 1:** Create and configure the tracker 72 | 73 | ``` C# 74 | // Expose services as static class to keep the example simple 75 | static class Services 76 | { 77 | // expose the tracker instance 78 | public static Tracker Tracker = new Tracker(); 79 | 80 | static Services() 81 | { 82 | // tell Jot how to track Window objects 83 | Tracker.Configure() 84 | .Id(w => w.Name) 85 | .Properties(w => new { w.Height, w.Width, w.Left, w.Top, w.WindowState }) 86 | .PersistOn(nameof(Window.WindowClosed)) 87 | } 88 | } 89 | ``` 90 | 91 | **Step 2:** Track the window instance 92 | 93 | ``` C# 94 | public MainWindow() 95 | { 96 | InitializeComponent(); 97 | 98 | // Start tracking the Window instance. 99 | // This will apply any previously stored data and start listening for "WindowClosed" event to persist new data. 100 | Services.Tracker.Track(this); 101 | } 102 | 103 | ``` 104 | 105 | That's it. We've set up tracking for all window objects in one place, so that all we need to to is call `tracker.Track(window)` on each window instance to preserve it's size and location. It's concise, the intent is clear, and there's no repetition. Notice also that we've mentioned each property only once, and it would be trivial to track additional properties. 106 | 107 | ### Real world form/window tracking 108 | 109 | The above code (both scenarios) works but it doesn't account for a few things. The first one is multiple displays. Screens can be unplugged, and we never want to position a window onto a screen that's no longer there. We can get around this problem very easily if we make the screen resolution part of the identifier. Jot will then track the same window separately for each screen configuration. 110 | 111 | #### WPF Window 112 | Here's how to properly track a WPF window: 113 | 114 | ``` csharp 115 | // 1. tell the tracker how to track Window objects (this goes in a startup class) 116 | tracker.Configure() 117 | .Id(w => w.Name, SystemInformation.VirtualScreen.Size) // <-- include the screen resolution in the id 118 | .Properties(w => new { w.Top, w.Width, w.Height, w.Left, w.WindowState }) 119 | .PersistOn(nameof(Window.Closing)) 120 | .StopTrackingOn(nameof(Window.Closing)); 121 | 122 | // 2. in the Window constructor 123 | public Window1() 124 | { 125 | // fetch the tracker instance e.g. via IOC or static property 126 | var tracker = Services.Tracker; 127 | tracker.Track(this); 128 | } 129 | ``` 130 | 131 | The `Id` method has a `params object []` parameter that can be used to define a namespace for the id. These parameters simply get *ToString-ed* and concatenated to the Id. By using the screen resolution as the namespace, we ensure that we maintain separate configurations for different resolutions. 132 | 133 | #### Windows forms 134 | 135 | Winforms have a few additional caveats: 136 | - Forms will return bogus size/location data for maximized/minimized forms, so we have to cancel persisting those 137 | - Tracking needs to be applied during `OnLoad` since `Top` and `Left` properties set in the constructor are ignored 138 | 139 | Here's how to properly track (Windows) Forms: 140 | 141 | ``` C# 142 | // tell the tracker how to track Form objects (this goes in a startup class) 143 | tracker.Configure() 144 | .Id(f => f.Name, SystemInformation.VirtualScreen.Size) // <-- include the screen resolution in the id 145 | .Properties(f => new { f.Top, f.Width, f.Height, f.Left, f.WindowState }) 146 | .PersistOn(nameof(Form.Move), nameof(Form.Resize), nameof(Form.FormClosing)) 147 | .WhenPersistingProperty((f, p) => p.Cancel = (f.WindowState != FormWindowState.Normal && (p.Property == nameof(Form.Height) || p.Property == nameof(Form.Width) || p.Property == nameof(Form.Top) || p.Property == nameof(Form.Left)))) // do not track form size and location when minimized/maximized 148 | .StopTrackingOn(nameof(Form.FormClosing)); // <-- a form should not be persisted after it is closed since properties will be empty 149 | 150 | // in the form code 151 | protected override void OnLoad(EventArgs e) 152 | { 153 | // fetch the tracker instance e.g. via IOC or static property 154 | var tracker = Services.Tracker; 155 | tracker.Track(this); 156 | } 157 | ``` 158 | 159 | #### Avalonia UI 160 | 161 | For [Avalonia UI](https://avaloniaui.net/) it is similar to WPF with some diferences: 162 | - The screen information is available from the Window class instead of being static. 163 | - Closing the app while being minimized would open the app next time also minimized if we don't check for that during window load. 164 | - When the app is minimized it reports a negative position instead of the position it would have if not minimized, 165 | so we need to check for negative position values during window load to prevent setting the window out of reach. 166 | 167 | ``` C# 168 | // in the Program.cs 169 | public static Tracker Tracker = new Tracker(); 170 | 171 | // in the Window constructor 172 | 173 | public MainWindow() 174 | { 175 | InitializeComponent(); 176 | #if DEBUG 177 | this.AttachDevTools(); 178 | #endif 179 | var initialPosition = this.Position; 180 | var trackerNamespace = string.Join("_", Screens.All.Select(s => s.WorkingArea.Size.ToString())); 181 | trackerNamespace += "__" + Environment.ProcessPath?.Replace("/", "_").Replace("\\", "_"); // <-- Add this if you want multiple copies of the same app to have different configurations. 182 | Program.Tracker.Configure() 183 | .Id(w => w.Name, trackerNamespace) 184 | .Properties(w => new { w.WindowState, w.Position, w.Width, w.Height }) 185 | .PersistOn(nameof(Window.Closing)) 186 | .StopTrackingOn(nameof(Window.Closing)); 187 | Program.Tracker.Track(this); 188 | 189 | if (this.WindowState == WindowState.Minimized) 190 | { 191 | this.WindowState = WindowState.Normal; 192 | } 193 | if (this.Position.X < -1 || this.Position.Y < -1) // -1 is used by Windows11 when docking windows on the side. 194 | { 195 | this.Position = initialPosition; 196 | } 197 | } 198 | ``` 199 | 200 | ## Which properties to track 201 | 202 | There are two methods (and several overloads) for telling Jot which properties of a given type to track. 203 | 204 | The `Properties` method accepts an expression that projects the target properties as an anonymous object: 205 | ```csharp 206 | tracker.Configure() 207 | .Properties(p => new 208 | { 209 | p.Name, 210 | p.LastName, 211 | MothersMaidenName = p.Mother.LastName // <-- can navigate object graph 212 | }) 213 | ``` 214 | The `Property` method is used to add propreties one by one. It allows specifying a name and a default value for each property. Since the property name can be passed as a string, this overload is useful for situations where the properties to track are determined at runtime. 215 | ```csharp 216 | tracker.Configure() 217 | .Property(p => p.Name) 218 | .Property(p => p.LastName) 219 | .Property(p => p.Age, -1) // if there's no value in the store, -1 will be set 220 | .Property(p => p.Mother.LastName, "MothersMaidenName") // <-- a name must be provided so it does not colide with p.LastName 221 | ``` 222 | 223 | The expressions you provide to these methods are used to specify which properties to track. The properties usually belong to the target object itself but they can also navigate through other objects (e.g. `p.Mother.LastName`). Based on these expressions, Jot will dynamically generate *getter* and *setter* methods for reading and writing the data. Both methods (`Properties` and `Property`) are cumulative: they add properties to track, rather than overwrite previous calls. 224 | 225 | 226 | ## When is the data persisted? 227 | 228 | Jot needs to know when a target's data has changed so it can save the updated data to the store. You can tell Jot to automatically persist a target whenever it (the target) fires an event: 229 | ```csharp 230 | tracker.Configure() 231 | .Properties(...) 232 | .PersistOn(nameof(Foo.SomeEvent)) <-- the event that should trigger persisting 233 | ``` 234 | You can optionally specify another object as the source of the event: 235 | ```csharp 236 | PersistOn("SomeEvent", otherObject) 237 | ``` 238 | You can also explicitly tell Jot to persist a target using the `Persist` method: 239 | ```csharp 240 | tracker.Persist(targetObj); 241 | ``` 242 | To tell Jot to persist all tracked objects, use the `PersistAll` method: 243 | ```csharp 244 | tracker.PersistAll(); 245 | ``` 246 | Usually, this would be during an application shutdown or at the end of a web request. Jot maintains a list of weak references to target objects. Targets that are already garbage collected are ignored. 247 | 248 | Some objects survive until the end of the application without being in a usable state. For example, a disposed form can still be referenced (and thus not garbage collected). We do not want to continue tracking that form after it is disposed because it will have bogus property values which we do not want to save to the store. For such cases, we can tell Jot to stop tracking a particular object by calling `StopTracking`: 249 | ```csharp 250 | tracker.StopTracking(targetObj); 251 | ``` 252 | We can also tell Jot to automatically stop tracking an object when it raises a certain event: 253 | ```csharp 254 | tracker.Configure() 255 | .Properties(...) 256 | .PersistOn(...) 257 | .StopTrackingOn(nameof(Form.Closed)) <-- the event that should cause the tracker to stop tracking the target 258 | ``` 259 | 260 | ## Where is the data stored? 261 | 262 | The `Tracker` class constructor has an optional parameter that allows you to specify where the data will be stored. 263 | 264 | ``` C# 265 | Tracker(IStore store) 266 | ``` 267 | Jot comes with a built-in implementation of `IStore` called `JsonFileStore`. If the `IStore` argument is not provided, the data will be stored in json files in the following folder: `%AppData%\[company name]\[application name]`. The *company name* and *application name* are read from the entry assembly's attributes). For each target object, there will be a separate file. Data is stored in separate files in order to make reading and writing data fast. 268 | 269 | To keep using the JSON file store, but store the data in a per-machine folder (e.g. `CommonApplicationData`), configure the tracker like so: 270 | 271 | ``` C# 272 | var tracker = new Tracker(new JsonFileStore(Environment.SpecialFolder.CommonApplicationData)); 273 | ``` 274 | 275 | Or specify the storage folder explicitly: 276 | 277 | ``` C# 278 | var tracker = new Tracker(new JsonFileStore(@"c:\example\path\")); 279 | ``` 280 | 281 | Here's what the stored data looks like: 282 | 283 | ![](http://i.imgur.com/xUVaVMh.png) 284 | 285 | ### Custom storage 286 | 287 | The `IStore` interface is very simple. For a given Id, it needs to be able to store and retrieve a dictionary of values. 288 | 289 | ```C# 290 | public interface IStore 291 | { 292 | void SetData(string id, IDictionary values); 293 | IDictionary GetData(string id); 294 | } 295 | ``` 296 | 297 | You can use this interface to make Jot store data anywhere you like e.g. in the cloud (to share settings for a user between machines) or a database. 298 | 299 | ## Value conversions and cancellation 300 | 301 | Jot lets you hook into the Apply and Persist operations. You can use this to perform value conversion and cancel persisting or applying data. As we've seen in the WinForms example, we can cancel applying size/location properties for Forms that are maximized or minimized: 302 | 303 | ```csharp 304 | tracker.Configure() 305 | .Id(...) 306 | .Properties(...) 307 | .WhenPersistingProperty((f, p) => p.Cancel = (f.WindowState != FormWindowState.Normal && (p.Property == nameof(Form.Height) || p.Property == nameof(Form.Width) || p.Property == nameof(Form.Top) || p.Property == nameof(Form.Left)))) 308 | ``` 309 | 310 | There are four hooks you can use: `WhenPersistingProperty`, `WhenApplyingProperty`, `WhenAppliedState` and `WhenPersisted`. 311 | 312 | ## Tracking and inheritance 313 | 314 | Tracking is configured per-type, meaning that a separate `TrackingConfiguration` object will need to be defined for each type of object we track. This configuration object tells Jot how to track objects of that type, but it also applies to objects of derived types. 315 | 316 | When configuring tracking for a derived type, Jot will examine the inheritance hierarchy of that type and look for the closest ancestor type for which a tracking configuration already exists. If it finds one, it will first create a copy of the base type's tracking configuration which you can then further customize. 317 | 318 | For example, let's suppose you define a class called `MyForm` that derives from `Form`. In addition to tracking the size and location, you also want to track the selected tab of a TabControl that's part of `MyForm`. Here's what that might look like: 319 | 320 | ``` csharp 321 | // configure tracking for Form 322 | tracker.Configure() 323 | .Id(f => f.Name, SystemInformation.VirtualScreen.Size) 324 | .Properties(f => new { f.Height, f.Width, f.Left, f.Top, f.WindowState}) 325 | .PersistOn(nameof(Form.Closing)) 326 | .StopTrackingOn(nameof(Form.Closed)) 327 | .WhenPersistingProperty((f, p) => p.Cancel = (f.WindowState != FormWindowState.Normal && p.Property != nameof(Form.WindowState))) 328 | 329 | 330 | // add the selected tab index for MyForm (everything else is already copied from the configuration for Form) 331 | tracker.Configure() 332 | .Properties(f => f.tabControl1.SelectedIndex); 333 | ``` 334 | We do not have to repeat the tracking configuration for size and location. Since `MyForm` derives from `Form`, the configuration for `MyForm` will be copied from the configuration for `Form` and we only need to add the additional `f.tabControl1.SelectedTabIndex` property. 335 | 336 | Furthermore, if we configure tracking for `Form` but not for `MyForm`, Jot will track `MyForm` instances using the tracking configuration for `Form`. 337 | 338 | ## The ITrackingAware interface 339 | 340 | Sometimes we cannot know at compile time which properties to track. In those situations, we need to configure tracking on a per-instance basis at runtime. To do this, our tracked objects can implement the `ITrackingAware` interface. 341 | 342 | ```csharp 343 | public interface ITrackingAware 344 | { 345 | void ConfigureTracking(TrackingConfiguration configuration); 346 | } 347 | ``` 348 | In the `ConfigureTracking` method, the object can dynamically specify which properties to track. The `configuration` parameter is specific to that instance (and not the type) so each instance can independently adjust its tracking configuration. 349 | 350 | For example, let's assume we have a form that has a datagrid, and we want to track the widths of grid columns. We could track each grid column object as a separate object, but we can also track those columns as part of tracking the form. Here's what that might look like: 351 | 352 | ```csharp 353 | public class MyFormWithDataGrid : ITrackingAware 354 | { 355 | protected override void OnLoad(EventArgs e) 356 | { 357 | Services.Tracker.Track(this); 358 | } 359 | 360 | public void InitConfiguration(TrackingConfiguration configuration) 361 | { 362 | // include data grid column widths when tracking this form 363 | for (int i = 0; i < dataGridView1.Columns.Count; i++) 364 | { 365 | var idx = i; // capture i into a variable (cannot use i directly since it changes in each iteration) 366 | configuration.Property("grid_c_" + dataGridView1.Columns[idx].Name, f => f.dataGridView1.Columns[idx].Width); 367 | } 368 | } 369 | } 370 | ``` 371 | 372 | # IOC integration 373 | 374 | Once we've explained to Jot how to track different types of objects, all that's needed in order for Jot to track instances of those types is to call: 375 | 376 | ``` C# 377 | tracker.Track(obj); 378 | ``` 379 | 380 | Here's the really cool part... When using an IOC container, many objects in the application will be created by the container. This gives us an opportunity to automatically track all created objects by hooking into the container. 381 | 382 | For example, with [SimpleInjector](https://simpleinjector.org/index.html) we can do this quite easily, with a single line of code: 383 | 384 | ``` C# 385 | var tracker = new Jot.Tracker(); 386 | var container = new SimpleInjector.Container(); 387 | 388 | //configure tracking and apply previously stored data to all created objects 389 | container.RegisterInitializer(d => { tracker.Track(d.Instance); }, cx => true); 390 | ``` 391 | 392 | With this in place, we can easily make any property of any object persistent, just by modifying the tracking configuration for its type. Neat! 393 | 394 | # Demos 395 | 396 | Demo projects for WPF and WinForms are included in the repository. 397 | 398 | 399 | # Contributing 400 | 401 | You can contribute to this project in the usual way: 402 | 403 | 1. First of all, don't forget to star the project 404 | 1. Fork the project 405 | 1. Push your commits to your fork 406 | 1. Make a pull request 407 | 408 | # TODO 409 | - Async support 410 | - IOC demos 411 | - aspnet core (demo + readme section) 412 | - readme topics: unity ioc integration, attribute based configuration 413 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate --------------------------------------------------------------------------------