├── .gitignore ├── LICENSE ├── README.md ├── linqpad-samples ├── Examples │ ├── Auto Charter.linq │ ├── FileOrder.txt │ ├── Loan Balance Calculator.linq │ ├── OpCode Browser.linq │ └── Spritesheet Cutter.linq ├── FileOrder.txt └── How To │ ├── Adding Editors (basic).linq │ ├── Adding Editors (less basic).linq │ ├── Adding Editors (non-global).linq │ ├── Anonymous Types.linq │ ├── Change Notifications.linq │ ├── Dumping Enumerable-likes.linq │ ├── Expansion (Anonymous Types).linq │ ├── Expansion (POCOs).linq │ ├── FileOrder.txt │ ├── POCO.linq │ └── Slider Editor.linq └── src ├── DumpEditable ├── DumpEditable.csproj ├── EditableDumpContainer.cs ├── EditorRule.cs ├── Editors.TextBox.cs ├── Editors.cs ├── Extensions.cs ├── Helpers │ ├── AnonymousObjectMutator.cs │ ├── CompositeDisposable.cs │ ├── DynamicTypeBuilder.cs │ └── TypeExtensions.cs └── Models │ ├── DumpEditableExpandAttribute.cs │ ├── DumpEditableOptions.cs │ ├── NullableOptionInclusionKind.cs │ └── PropertyEditor.cs └── linqpad-dump-editable.sln /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ryan Davis 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DumpEditable 2 | DumpEditable is an extensible 'inline editor' extension for [LINQPad](https://www.linqpad.net), with `Dump`-like semantics. It allows you to dump an editable representation of an object onto the results view - useful for quickly adding interactivity to a query without having to spend time arranging and wiring up controls. 3 | 4 | DumpEditable aims to provide reasonable defaults out of the box but be flexible enough for you to customise it to your specific needs. If you install it into your 'My Extensions', you can build up a library of tailored editors available to all your queries. Or, you can just add extensions that make sense for any particular query. 5 | 6 | ## Installation 7 | 8 | ⚠🚨**DumpEditable is currently alpha quality and probably has bugs and things I havent thought about! Take care before using it in important/Production queries**🚨⚠ 9 | 10 | - Add the [`DumpEditable.LINQPad` NuGet Package](https://nuget.org/packages/DumpEditable.LINQPad) to your query or My Extensions. 11 | 12 | - Add `LINQPad.DumpEditable` to your using namespaces. 13 | 14 | DumpEditable comes with LINQPad samples, including basic 'How To' guides and a few more complete demos. When you install the package into your query, these samples will show up automatically in the samples pane. 15 | 16 | ## Usage 17 | 18 | This section outlines the basic concepts available in DumpEditable. The LINQPad samples provide more detail. 19 | 20 | ### Showing an editor 21 | Getting an editor onto the results pane is as easy as calling `.DumpEditable()` on an object. `DumpEditable()` returns the input object so can be chained similarly to LINQPad's `Dump()`: 22 | 23 | ![basic poco dumped with editor displayed](https://ryandavis.io/content/images/2019/05/dump-editable/basic.png) 24 | Here we see we got a basic editor for our `Pet` object without writing any code! Clicking any of the properties will allow us to modify the property values. 25 | 26 | ### Handling changes 27 | In some cases, you may be running some kind of loop in your query which will give you the opportunity to read new values on your changed object. In many cases though, you'll want to be notified when a change occurs. You can do this by taking a reference to the `EditableDumpContainer` - an optional `out` parameter on `DumpEditable` - and adding change handlers. There are three ways you can register for change notifications - indiscriminate, reflection-based, and strongly typed - you can pick the one that best fits a given use case. 28 | 29 | ![demonstration of adding change handlers](https://ryandavis.io/content/images/2019/04/dump-editable/change-handling.png) 30 | 31 | ### Dumping collections 32 | 33 | DumpEditable allows you to dump `IEnumerable`s using the `DumpEditableEnumerable()` extension. Many times you'll want to be dumping a 'materialised' collection (a `List`, array, etc. ) - if you dump an enumerable proper it will be re-evaluated after every change, and if that results in new objects, the changes made to the old objects will not be apparent. However, there are some situations in which re-evaluation can be useful. 34 | 35 | ![enumerable dump output in results pane](https://ryandavis.io/content/images/2019/05/dump-editable/enumerable.png) 36 | When adding change handlers to dumped collections you can use the object parameters on `OnPropertyValueChanged` `(*obj*, prop, val) => ` and `AddChangeHandler (*obj*, value) => ` to know which item was affected. 37 | 38 | ### Dumping anonymous types 39 | 40 | In C#, anonymous types are read-only. However, defining and modifying anonymous type instances would be super convenient for interactive LINQPad queries, so DumpEditable makes it possible. 41 | 42 | ![anonymous type dump output in results pane](https://ryandavis.io/content/images/2019/05/dump-editable/anonymous.png) 43 | Modifying instances of anonymous types can cause problems in the Real World, because methods like `GetHashCode()` and `Equals()` - which benefit from the assumption that anonymous types are read only - can become incorrect. For basic editor control scenarios this is not a problem - but if you're doing anything more advanced, remember to be aware of the implications. 44 | 45 | ### Adding custom editors 46 | 47 | DumpEditable works by evaluating each property of the target object against `EditorRule`s to decide whether a specific editor should be displayed. If no `EditorRule`s match for a given property, the default LINQPad output will be displayed instead. DumpEditable ships with basic editors for primitives and enums. You can extend DumpEditable by adding custom `EditorRule`s, which will be evaluated prior to the built-ins (LIFO). Rules consist of a `match` function, which decides whether the rule should apply to a given object property, and a `getEditor` function, which provides the editor content that will be displayed for the object property. The editor content should include any functionality required to get new values. 48 | 49 | `EditorRule` has a few helper methods you can use to make things easier. For example, the aptly named `EditorRule.ForTypeWithStringBasedEditor(Func parseFunc)` lets you provide a type parameter `T` and a `TryParse`-style `string -> T` conversion function, and gives you the rest - a basic text box implementation - for free. In the below case, we add support for editing `Guid`s: 50 | ![adding a basic guid editor rule](https://ryandavis.io/content/images/2019/05/dump-editable/editor-rule-basic.png) 51 | 52 | For more control, you can implement an `EditorRule` from scratch, like in the below case where we match on a specific type's property, and provide an editor with images for content: 53 | 54 | ![adding a food selector rule](https://ryandavis.io/content/images/2019/04/dump-editable/editor-rule-foodselector.png) 55 | When creating editor rules, it's important to use the provided `setVal` callback, rather than applying reflection directly - the `setVal` callback encapsulates the functionality required to properly change a value, including refreshing the editor contents and applying special processing such as anonymous type mutation. If you want to suppress automatic refresh of an editor - for example, to avoid recreating a control - you can set `DisableAutomaticRefresh` of the editor rule to `false`, or specify it via the optional `disableAutomaticRefresh` parameter on `EditorRule.For` or `EditorRule.ForType`. 56 | 57 | In both of the examples above we added 'global rules' that apply to all `EditableDumpContainer`s. You can also add a rule to an individual instance using the `AddEditorRule` method on that instance. Instance rules are evaluated before global rules. 58 | 59 | DumpEditable also includes a slider control wrapper that you can pass to `getEditor`, accessible via the `Editors.Slider` overloads. 60 | 61 | ![using a slider editor](https://ryandavis.io/content/images/2019/05/dump-editable/slider-editor.png) 62 | You can provide a minimum and maximum value using the appropriate overload, or rely on the "*" settings button at runtime. As the LINQPad slider works with integers only, using it with types other than integers requires you to provide `Func toInt` and `Func fromInt` conversion parameters. Note that your conversion function does not need to result in an `int` - rather, it must result in something that can be turned into an `int` using `Convert.ToInt32`, which DumpEditable will call internally. This saves you from needing to pollute your code with casts. A typical use of the conversion functions might be to specify a percentage-based slider by multiplying/dividing a `float` or `double` by 100. 63 | 64 | ![using a slider conversion function](https://ryandavis.io/content/images/2019/05/dump-editable/slider-percent.png) 65 | There might be other clever ways to make use of this functionality. 66 | 67 | ### Nested object editor expansion 68 | 69 | As you might have noticed earlier, nested objects within anonymous type instances are made editable automatically. Solving this problem for POCOs requires a bit more smarts (for example, handling of reference loops), which I haven't added yet, so nested objects on POCOs are not automatically evaluated. In the meantime, it's possible to add rules using the `EditorRule.ForExpansion(Func match)` helper, which will cause any matching properties to be expanded with an editor. 70 | 71 | ## Things to watch for 72 | 73 | - this hasn't been tried against any data contexts, database models etc. It might work fine; it might do Bad things like try to pull entire tables into memory. Use at your own risk 74 | 75 | - DumpEditable requires the query to 'keep running' (`Util.KeepRunning`) for property changes to work nicely after your query body has finished executing (which is likely to occur if you're using an event-based model). By default, DumpEditable automatically calls `Util.KeepRunning` for each running container. If you want to manage query lifetime yourself, can disable this behaviour by setting `EditableDumpContainer.DefaultOptions.AutomaticallyKeepQueryRunning` to `false` before dumping your first object. 76 | 77 | - Because it's early days, DumpEditable will throw if it encounters an error when trying to generate an editor output. If you don't like this and would prefer it to fall back to displaying the usual LINQPad output, you can make sure `EditableDumpContainer.DefaultOptions.FailSilently` is set to `true` before dumping the offending object. 78 | 79 | - The source contains some scary type signatures that could probably be improved (named delegates?) and some of them leak into the extension API. While this is c.WithEditorRule(..) 98 | .AddChangeHandler(..)) 99 | ``` 100 | 101 | ## Contributions 102 | 103 | Welcome! Open an issue to discuss what you're thinking about. 104 | 105 | ## Acknowledgements 106 | 107 | * The dynamic type generation and anonymous object mutation features were adapted from implementations found in (of course) StackOverflow questions. These are mentioned in the source 108 | 109 | * `CompositeDisposal` implementation taken from [System.Reactive](https://github.com/dotnet/reactive) 110 | 111 | * Thanks to [Joe Albahari](https://twitter.com/linqpad?lang=en), the creator of LINQPad for creating and continuing to improve such a uniquely powerful, flexible and performant tool. 112 | -------------------------------------------------------------------------------- /linqpad-samples/Examples/Auto Charter.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\Source\Repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | System.Drawing 4 | System.Threading.Tasks 5 | System.Windows.Forms.DataVisualization.Charting 6 | LINQPad.DumpEditable 7 | 8 | 9 | async Task Main() 10 | { 11 | var charter = GetCharter(); 12 | 13 | var data = new[] 14 | { 15 | new { Series = 'A', Value = 0.0 }, 16 | new { Series = 'B', Value = 1.0 }, 17 | new { Series = 'C', Value = 2.0 }, 18 | }.ToList(); 19 | 20 | var editor = EditableDumpContainer.ForEnumerable(data); 21 | var chart = new DumpContainer(); 22 | 23 | Util.VerticalRun( 24 | Util.Metatext("Update the values of the series to change the values being plotted"), 25 | Util.Metatext(Util.IsDarkThemeEnabled ? "" : "(This probably looks better in dark theme)"), 26 | Util.HorizontalRun(true, chart, 27 | Util.VerticalRun( 28 | new Hyperlinq(() => 29 | { 30 | data.Add(new { Series = (char)(data.Last().Series + 1), Value = data.Last().Value + 1 }); 31 | editor.Refresh(); 32 | }, "add series"), 33 | editor)) 34 | ).Dump(); 35 | 36 | Bitmap last = null; 37 | while(true) 38 | { 39 | await Task.Delay(TimeSpan.FromSeconds(.25)); 40 | 41 | chart.Content = charter(data.Select(x => ($"{x.Series}", x.Value))); 42 | last?.Dispose(); 43 | last = (Bitmap)(chart.Content); 44 | } 45 | } 46 | 47 | public Func, Bitmap> GetCharter() 48 | { 49 | var chart = new Chart(); 50 | var ca = chart.ChartAreas.Add("ca"); 51 | chart.Width = (1920 / 2); 52 | chart.Height = (1080 / 2); 53 | 54 | if (Util.IsDarkThemeEnabled) 55 | StyleDark(chart, ca); 56 | else 57 | StyleLight(chart, ca); 58 | 59 | return vs => 60 | { 61 | foreach (var v in vs) 62 | { 63 | var series = chart.Series.FirstOrDefault(x => x.Name == v.series) ?? chart.Series.Add(v.series); 64 | series.ChartType = SeriesChartType.FastLine; 65 | series.BorderWidth = Util.IsDarkThemeEnabled ? 1 : 3; 66 | 67 | series.Points.AddXY(DateTime.Now.ToOADate(), v.val); 68 | } 69 | 70 | var ms = new MemoryStream(); 71 | chart.SaveImage(ms, ChartImageFormat.Png); 72 | ms.Seek(0, SeekOrigin.Begin); 73 | 74 | return new Bitmap(ms); 75 | }; 76 | } 77 | 78 | public void StyleDark(Chart chart, ChartArea ca) 79 | { 80 | chart.BackColor = Color.FromArgb(255, 30, 30, 30); 81 | 82 | ca.BackColor = Color.FromArgb(255, 32, 32, 32); 83 | ca.AxisY.Minimum = 0.0; 84 | ca.AxisY.LineColor = Color.LightGray; 85 | ca.AxisX.LineColor = Color.LightGray; 86 | ca.AxisY.LabelStyle.ForeColor = Color.LightGray; 87 | ca.AxisX.LabelStyle.ForeColor = Color.LightGray; 88 | ca.AxisY.LabelStyle.Format = ""; 89 | ca.AxisX.LabelStyle.Enabled = false; 90 | ca.AxisY.MajorGrid.Enabled = false; 91 | ca.AxisY.MinorGrid.Enabled = false; 92 | ca.AxisX.MajorGrid.Enabled = false; 93 | ca.AxisX.MinorGrid.Enabled = false; 94 | } 95 | 96 | public void StyleLight(Chart chart, ChartArea ca) 97 | { 98 | ca.BackColor = Color.FromArgb(255, 220, 220, 220); 99 | ca.AxisY.Minimum = 0.0; 100 | ca.AxisY.LabelStyle.Format = ""; 101 | ca.AxisX.LabelStyle.Enabled = false; 102 | ca.AxisY.MajorGrid.Enabled = false; 103 | ca.AxisY.MinorGrid.Enabled = false; 104 | ca.AxisX.MajorGrid.Enabled = false; 105 | ca.AxisX.MinorGrid.Enabled = false; 106 | } -------------------------------------------------------------------------------- /linqpad-samples/Examples/FileOrder.txt: -------------------------------------------------------------------------------- 1 | Auto Charter.linq 2 | Spritesheet Cutter.linq 3 | Loan Balance Calculator.linq 4 | OpCode Browser.linq -------------------------------------------------------------------------------- /linqpad-samples/Examples/Loan Balance Calculator.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\Source\Repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | LINQPad.Controls 5 | System.Drawing 6 | 7 | 8 | void Main() 9 | { 10 | // basic loan balance projector 11 | // this one stresses LINQPad a bit 12 | var loanConfig = new LoanConfig 13 | { 14 | InitialBalance = 750000, 15 | Years = 30, 16 | Rate = 0.065, 17 | }; 18 | 19 | var ed = EditableDumpContainer.For(loanConfig); 20 | var output = new DumpContainer(); 21 | 22 | Util.HorizontalRun(true, ed, output).Dump(); 23 | 24 | ed.AddEditorRule( 25 | EditorRule.For((o, p) => p.Name == nameof(loanConfig.InitialBalance), 26 | Editors.Slider(250000.0, 5000000.0, x => x, x => x), 27 | true)); 28 | 29 | ed.AddEditorRule( 30 | EditorRule.For((o, p) => p.Name == nameof(loanConfig.Years), 31 | Editors.Slider(1, 50), 32 | true)); 33 | 34 | ed.AddEditorRule( 35 | EditorRule.For((o, p) => p.Name == nameof(loanConfig.Rate), 36 | Editors.Slider(0.025, 0.15, x => x * 100, x => x / 100.0), 37 | true)); 38 | 39 | ed.OnChanged += () => 40 | { 41 | var bals = GetBalanceSchedule(loanConfig); 42 | var chart = Util.Chart(bals, x => x.period, x => x.balance, LINQPad.Util.SeriesType.Line).ToBitmap(); 43 | 44 | // since the sliders are multi-threaded this can fail sometimes 45 | try { output.Content = chart; } catch { } 46 | }; 47 | 48 | ed.OnChanged(); 49 | } 50 | 51 | public List<(int period, double balance)> GetBalanceSchedule(LoanConfig loanConfig) 52 | { 53 | var freqMultiplier = 54 | loanConfig.RepaymentFrequency == RepaymentFrequency.Fortnightly 55 | ? 26 56 | : 12; 57 | 58 | var balance = loanConfig.InitialBalance; 59 | var periods = loanConfig.Years * freqMultiplier; 60 | var rate = loanConfig.Rate / freqMultiplier; 61 | var payment = (rate / (1 - (Math.Pow((1 + rate), -(periods))))) * balance; 62 | 63 | return Enumerable 64 | .Range(0, periods) 65 | .Select(i => 66 | { 67 | var interestForMonth = balance * rate; 68 | var principalForMonth = payment - interestForMonth; 69 | 70 | balance += interestForMonth; 71 | balance -= payment; 72 | 73 | return (i, Math.Round(balance,0) ); 74 | }) 75 | .ToList(); 76 | } 77 | 78 | public class LoanConfig 79 | { 80 | public double InitialBalance { get; set; } 81 | public int Years { get; set; } 82 | public double Rate { get; set; } 83 | public RepaymentFrequency RepaymentFrequency { get; set; } 84 | } 85 | 86 | public enum RepaymentFrequency 87 | { 88 | Fortnightly, 89 | Monthly, 90 | } -------------------------------------------------------------------------------- /linqpad-samples/Examples/OpCode Browser.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\Source\Repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Reflection.Emit 5 | 6 | 7 | void Main() 8 | { 9 | EditableDumpContainer.DefaultOptions.StringBasedEditor = 10 | Editors.TextBoxBasedStringEditor(liveUpdates: true); 11 | 12 | var filter = new OpCodeFilter().DumpEditable(out var editor); 13 | var opcodes = new DumpContainer().Dump(); 14 | var selectedOpcode = new DumpContainer() { DumpDepth = 2 }.Dump(); 15 | 16 | var filteredOpcodes = 17 | AllOpCodes 18 | .Where(filter.Matches) 19 | .Select(o => new Hyperlinq(() => selectedOpcode.Content = o, o.Name)); 20 | 21 | editor.OnChanged += () => opcodes.Content = Util.HorizontalRun(true, filteredOpcodes); 22 | editor.OnChanged(); 23 | } 24 | 25 | public IEnumerable AllOpCodes 26 | => typeof(OpCodes) 27 | .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Default) 28 | .Select(f => (OpCode) f.GetValue(null)); 29 | 30 | public class OpCodeFilter 31 | { 32 | public string Name { get; set; } 33 | public OpCodeType? OpCodeType { get; set; } 34 | public FlowControl? FlowControl { get; set; } 35 | public OperandType? OperandType{ get; set; } 36 | 37 | public bool Matches(OpCode o) 38 | => (String.IsNullOrWhiteSpace(Name) || o.Name.ToLower().Contains(Name.ToLower())) 39 | && (OpCodeType is null || o.OpCodeType == OpCodeType) 40 | && (FlowControl is null || o.FlowControl == FlowControl) 41 | && (OperandType is null || o.OperandType == OperandType); 42 | } -------------------------------------------------------------------------------- /linqpad-samples/Examples/Spritesheet Cutter.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\Source\Repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | <RuntimeDirectory>\WPF\WindowsBase.dll 4 | <RuntimeDirectory>\System.Xaml.dll 5 | <RuntimeDirectory>\Accessibility.dll 6 | <RuntimeDirectory>\System.Security.dll 7 | <RuntimeDirectory>\System.Configuration.dll 8 | LINQPad.DumpEditable 9 | System.Net.Http 10 | System.Threading.Tasks 11 | System.Drawing 12 | System.Windows 13 | 14 | 15 | async Task Main() 16 | { 17 | // this demo shows using an EditableDumpContainer to trial-an-error cutting up of a spritesheet 18 | var url = "http://tsgk.captainn.net/dld.php?s=custom&f=xander_marioandluigi_sheet.png"; 19 | var bs = await new HttpClient().GetByteArrayAsync(url); 20 | var img = new Bitmap(new MemoryStream(bs)); 21 | var (w,h) = (img.Width, img.Height); 22 | 23 | var config = new SpriteCuttingConfig 24 | { 25 | XOffset = 10, 26 | YOffset = 0, 27 | SpriteWidth = 30, 28 | SpriteHeight = 39, 29 | Rows = 6, 30 | Cols = 8, 31 | }; 32 | 33 | var editor = EditableDumpContainer.For(config); 34 | var output = new DumpContainer().Dump(); 35 | 36 | editor.OnChanged += () => 37 | { 38 | var width = config.SpriteWidth * config.Cols; 39 | var height = config.SpriteHeight * config.Rows; 40 | 41 | var spritesArea = img.CropAtRect(new Rectangle(config.XOffset, config.YOffset, width, height)); 42 | var spriteRects = new List(); 43 | 44 | var sprites = 45 | Enumerable 46 | .Range(0, config.Rows) 47 | .SelectMany(y => Enumerable.Range(0, config.Cols), 48 | (y, x) => 49 | Util.VerticalRun( 50 | spritesArea.CropAtRect( 51 | new Rectangle( 52 | x * config.SpriteWidth, 53 | y * config.SpriteHeight, 54 | config.SpriteWidth, 55 | config.SpriteHeight).Do(spriteRects.Add) 56 | ), $"{x},{y}")) 57 | .ToList(); 58 | 59 | var regionsInOriginalSpace = 60 | spriteRects 61 | .Select(r => new Rectangle(r.X + config.XOffset, r.Y + config.YOffset, r.Width, r.Height)) 62 | .ToList(); 63 | 64 | var text = config.IsCorrect() ? GoodText : BadText; 65 | var color = config.IsCorrect() ? Color.Green : Color.Red; 66 | var display = new Bitmap(img).MarkRegions(regionsInOriginalSpace, color); 67 | editor.Refresh(); 68 | output.Content = Util.VerticalRun(Util.Metatext(text), 69 | Util.HorizontalRun(true, editor, display), 70 | "", "", Util.Metatext("Results:"), "", 71 | Util.HorizontalRun(true, sprites)); 72 | }; 73 | 74 | editor.OnChanged(); 75 | } 76 | 77 | public class SpriteCuttingConfig 78 | { 79 | public short XOffset { get; set; } 80 | public short YOffset { get; set; } 81 | public short SpriteWidth { get; set; } 82 | public short SpriteHeight { get; set; } 83 | public short Rows { get; set; } 84 | public short Cols { get; set; } 85 | } 86 | 87 | const string BadText = 88 | "Our sprite sizes seem to be ok, but we're missing some sprites!\r\n" + 89 | "Maybe we need to fiddle with the X Offset and Rows/Cols a little...\r\n\r\n"; 90 | 91 | const string GoodText = 92 | "Great! You did it!\r\n\r\n"; 93 | 94 | public static class Ext 95 | { 96 | public static bool IsCorrect(this SpriteCuttingConfig c) 97 | => c.XOffset == 30 && c.YOffset == 0 98 | && c.SpriteWidth == 30 && c.SpriteHeight == 39 99 | && c.Rows == 9 && c.Cols == 12; 100 | 101 | public static Bitmap CropAtRect(this Bitmap b, Rectangle r) 102 | { 103 | Bitmap nb = new Bitmap(r.Width, r.Height); 104 | Graphics g = Graphics.FromImage(nb); 105 | g.DrawImage(b, -r.X, -r.Y); 106 | return nb; 107 | } 108 | 109 | public static Bitmap MarkRegions(this Bitmap b, List regions, Color color) 110 | { 111 | var p = new Pen(color); 112 | 113 | using (var g = Graphics.FromImage(b)) 114 | foreach (var r in regions) 115 | g.DrawRectangle(p, r); 116 | 117 | return b; 118 | } 119 | 120 | public static IEnumerable Do(this IEnumerable items, Action action) 121 | { 122 | foreach (var item in items) 123 | { 124 | action(item); 125 | yield return item; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /linqpad-samples/FileOrder.txt: -------------------------------------------------------------------------------- 1 | How To 2 | Examples -------------------------------------------------------------------------------- /linqpad-samples/How To/Adding Editors (basic).linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // You can add your own "editors" to DumpEditable by specifying a rule function and providing an implementation 10 | // EditorRule has some helpers or you can write one from scratch 11 | 12 | // note how we can't edit a guid because there's no editor implementation 13 | "".Dump("Can't edit, no guid editor implementation"); 14 | new { TheGuid = Guid.NewGuid() }.DumpEditable(); 15 | 16 | // let's add one 17 | // the string-based editor that most default items are based on has a an easy to use helper 18 | // you specify the type and a 'parseFunc' that returns true/false depending on whether the string could be parsed 19 | // and has an out T parameter that you set to the result. This mirrors the 'TryParse' pattern found in across .NET BCL 20 | // so it's easy to add a Guid editor: 21 | var guidEditor = EditorRule.ForTypeWithStringBasedEditor(Guid.TryParse); 22 | 23 | // you can add a rule to the global rules 24 | EditableDumpContainer.AddGlobalEditorRule(guidEditor); 25 | 26 | // now we can edit a guid! 27 | "".Dump("Can edit, we added an implementation!"); 28 | new { TheGuid = Guid.NewGuid() }.DumpEditable(); 29 | 30 | // installing DumpEditable in your MyExtensions and adding rules there can make them available by default to all queries! 31 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Adding Editors (less basic).linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // You can add an editor implementation from scratch to have more influence over matching and input. 10 | var rule = EditorRule.For( 11 | rule: (obj, prop) => // return true from here if this editor should be used 12 | { 13 | // here we _only_ want to be used for the FavouriteFood property on pet 14 | return obj is Pet && prop.Name == nameof(Pet.FavouriteFood); 15 | }, 16 | getEditor: (obj, prop, getVal, setVal) => // return the object that should be displayed for the property and that can take input 17 | { 18 | // here we limit the user to three specific values designated by the images 19 | var foods = new [] 20 | { 21 | ("pasta", "https://image.flaticon.com/icons/png/128/123/123315.png"), 22 | ("pizza", "http://icons.iconarchive.com/icons/sonya/swarm/128/Pizza-icon.png"), 23 | ("ice cream", "https://img.icons8.com/dusk/2x/ice-cream-cone.png") 24 | }; 25 | 26 | return 27 | Util.VerticalRun(getVal(), // <- we can get the current value using getVal() 28 | Util.HorizontalRun(true, 29 | foods.Select(f => Util.VerticalRun( 30 | Util.Image(f.Item2), 31 | new Hyperlinq(() => setVal(f.Item1), // <- we use setVal to update the property. 32 | // we should use setVal rather than reflection on obj/prop directly 33 | // because setVal handles things like modifying anonymous types 34 | // and refreshing the editor for us. 35 | " - select - "))))); 36 | }); 37 | 38 | EditableDumpContainer.AddGlobalEditorRule(rule); 39 | 40 | new Pet 41 | { 42 | Name = "Rover", 43 | FavouriteFood = "pizza" 44 | } 45 | .DumpEditable(); 46 | } 47 | 48 | 49 | public class Pet 50 | { 51 | public string Name { get; set; } 52 | public string FavouriteFood { get; set; } 53 | } 54 | -------------------------------------------------------------------------------- /linqpad-samples/How To/Adding Editors (non-global).linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // Previous samples showed adding 'global editors' which are used by any EditableDumpContainers 10 | // We can add editors to individual EditableDumpContainers too 11 | 12 | // create an editor that will be lucky enough to be able to edit guids 13 | "".Dump("will be able to edit guids"); 14 | new { TheGuid = Guid.NewGuid() }.DumpEditable(out var editorThatWillHaveAGuidEditor); 15 | 16 | // create our favourite guid editor 17 | var guidEditor = EditorRule.ForTypeWithStringBasedEditor(Guid.TryParse); 18 | 19 | // add it to a specific EditableDumpContainer 20 | editorThatWillHaveAGuidEditor.AddEditorRule(guidEditor); 21 | 22 | // create an editor that wont be able to edit guids 23 | "".Dump("wont be able to edit guids"); 24 | new { TheGuid = Guid.NewGuid() }.DumpEditable(out var editorThatWillNotHaveAGuidEditor); 25 | 26 | // observe that our "will have" editor has an editor, but our "wont have" doesnt! 27 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Anonymous Types.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // DumpEditable lets you modify anonymous types (even though anonymous types are read-only) 10 | // See how we can update this anonyPet that looks a lot like Pet but is really an anonymous type 11 | var anonyPet = new 12 | { 13 | Name = "King", 14 | Kind = AnimalKind.Doggo, 15 | DateOfBirth = DateTime.Today.AddYears(-10), 16 | IsFirstPet = true, 17 | FavouriteActivities = new [] { "eating", "sleeping", "saving the world" }, 18 | }.DumpEditable(out var editor); 19 | 20 | editor.AddChangeHandler(x => x.Kind, (p, v) => $"AnonyPet just turned into a {v}!".Dump()); 21 | 22 | // Modifying anonymous types is Fine for the kind of things you're likely to use them for with DumpEditable 23 | // but keep in mind that Bad Things will probably happen if you modify instances of anonymous types that 24 | // end up in Dictionaries, HashSets, etc. 25 | } 26 | 27 | public enum AnimalKind 28 | { 29 | Doggo, 30 | Cat, 31 | Birb 32 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Change Notifications.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | 5 | 6 | void Main() 7 | { 8 | // you can respond to changes being made to your object by taking a reference to the EditableDumpContainer 9 | // it is available as an out parameter of the DumpEditable method, or you can create a EditableDumpContainer by hand 10 | var pet = new Pet 11 | { 12 | Name = "King", 13 | Kind = AnimalKind.Doggo, 14 | DateOfBirth = DateTime.Today.AddYears(-10), 15 | IsFirstPet = true, 16 | FavouriteActivities = { "eating", "sleeping", "saving the world" }, 17 | }.DumpEditable(out var editor); // <-- here we get a reference to the editor 18 | 19 | // if you don't care what changed, just that something changed, you can implement OnChanged 20 | editor.OnChanged += () => "A change occurred!".Dump(); 21 | 22 | // if you want to know about property changes and values, you can implement OnPropertyValueChanged, which gives you the object, propertyinfo and new value 23 | editor.OnPropertyValueChanged += (o, p, v) => Util.HorizontalRun(true, $"The value for '{p.Name}' on '{o}' changed to: ", v).Dump(); 24 | 25 | // if you want strongly-typed notifications for specific property changes, you can use AddChangeHandler 26 | editor.AddChangeHandler(x => x.FavouriteActivities, (o, activities) => $"{nameof(Pet.FavouriteActivities)} changed and the first favourite activity is now '{activities.First()}'".Dump()); 27 | 28 | // as you'll see, change notifications are fired in order of specificity, most specific to least 29 | } 30 | 31 | public class Pet 32 | { 33 | public string Name { get; set; } 34 | public AnimalKind Kind { get; set; } 35 | public DateTimeOffset DateOfBirth { get; set; } 36 | public bool? IsFirstPet { get; set; } = true; 37 | public List FavouriteActivities { get; set; } = new List(); 38 | 39 | public Guid Tag { get; set; } = Guid.NewGuid(); 40 | } 41 | 42 | public enum AnimalKind 43 | { 44 | Doggo, 45 | Cat, 46 | Birb 47 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Dumping Enumerable-likes.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // DumpEditable might do a reasonable job at dumping lists of items as well 10 | // use DumpEditableEnumerable instead of DumpEditable 11 | 12 | var animalKinds = Enum.GetValues(typeof(AnimalKind)).OfType().ToList(); 13 | 14 | var anonyPets = 15 | Enumerable 16 | .Range(0, 5) 17 | .Select(i => 18 | new 19 | { 20 | Name = $"King {i}", 21 | Kind = animalKinds[i % animalKinds.Count], 22 | DateOfBirth = DateTime.Today.AddYears(-(i + 1)), 23 | IsFirstPet = i == 0, 24 | FavouriteActivities = new [] { "eating", "sleeping", "saving the world", "more eating", "more sleeping" }.Take(i), 25 | }) 26 | .ToList() // if you don't materialise an ienumerable you'll have a bad time 27 | .DumpEditableEnumerable(out var editor); 28 | 29 | 30 | } 31 | 32 | public enum AnimalKind 33 | { 34 | Doggo, 35 | Cat, 36 | Birb 37 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Expansion (Anonymous Types).linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // Anonymous type properties can be expanded automatically 10 | var anonyPet = new 11 | { 12 | Name = "King", 13 | Kind = AnimalKind.Doggo, 14 | DateOfBirth = DateTime.Today.AddYears(-10), 15 | IsFirstPet = true, 16 | FavouriteActivities = new [] { "eating", "sleeping", "saving the world" }, 17 | InnerObject = new { 18 | InnerPropertyA = "A", 19 | InnerProperyB = 12 20 | } 21 | }.DumpEditable(out var editor); 22 | } 23 | 24 | public enum AnimalKind 25 | { 26 | Doggo, 27 | Cat, 28 | Birb 29 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Expansion (POCOs).linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | LINQPad.DumpEditable.Models 6 | 7 | 8 | async Task Main() 9 | { 10 | // since I haven't worked out a great way to do it yet, you can manually add `DumpEditableExpand` 11 | // to any complex property on a POCO that you'd like DumpEditable to recursively allow editing of 12 | var pet = new Pet 13 | { 14 | Name = "King", 15 | Kind = AnimalKind.Doggo, 16 | DateOfBirth = DateTime.Today.AddYears(-10), 17 | IsFirstPet = true, 18 | FavouriteActivities = { "eating", "sleeping", "saving the world" }, 19 | }.DumpEditable(); 20 | } 21 | 22 | public class Pet 23 | { 24 | public string Name { get; set; } 25 | public AnimalKind Kind { get; set; } 26 | public DateTimeOffset DateOfBirth { get; set; } 27 | public bool? IsFirstPet { get; set; } = true; 28 | public List FavouriteActivities { get; set; } = new List(); 29 | 30 | [DumpEditableExpand] // <- this makes it expand 31 | public PetTag WillBeEditableBecauseHasAttribute { get; set; } = new PetTag { }; 32 | 33 | public PetTag WontBeEditableBecauseDoesNotHaveAtribute { get; set; } = new PetTag { }; 34 | } 35 | 36 | public class PetTag 37 | { 38 | public string Identifier { get; set; } 39 | public string Material { get; set; } 40 | } 41 | 42 | public enum AnimalKind 43 | { 44 | Doggo, 45 | Cat, 46 | Birb 47 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/FileOrder.txt: -------------------------------------------------------------------------------- 1 | POCO.linq 2 | Change Notifications.linq 3 | Anonymous Types.linq 4 | Expansion (POCOs).linq 5 | Expansion (Anonymous Types).linq 6 | Dumping Enumerable-likes.linq 7 | Adding Editors (basic).linq 8 | Adding Editors (less basic).linq 9 | Adding Editors (non-global).linq 10 | Slider Edutir.linq 11 | -------------------------------------------------------------------------------- /linqpad-samples/How To/POCO.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\source\repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | System.Threading.Tasks 5 | 6 | 7 | async Task Main() 8 | { 9 | // the simplest demonstration is the dumping of a plain old C# object 10 | // like LINQPad's Dump, DumpEditable returns the original object so can typically be chained in the same manner 11 | var pet = new Pet 12 | { 13 | Name = "King", 14 | Kind = AnimalKind.Doggo, 15 | DateOfBirth = DateTime.Today.AddYears(-10), 16 | IsFirstPet = true, 17 | FavouriteActivities = { "eating", "sleeping", "saving the world" }, 18 | }.DumpEditable(); 19 | } 20 | 21 | public class Pet 22 | { 23 | public string Name { get; set; } 24 | public AnimalKind Kind { get; set; } 25 | public DateTimeOffset DateOfBirth { get; set; } 26 | public bool? IsFirstPet { get; set; } = true; 27 | public List FavouriteActivities { get; set; } = new List(); 28 | 29 | public Guid Tag { get; set; } = Guid.NewGuid(); 30 | } 31 | 32 | public enum AnimalKind 33 | { 34 | Doggo, 35 | Cat, 36 | Birb 37 | } -------------------------------------------------------------------------------- /linqpad-samples/How To/Slider Editor.linq: -------------------------------------------------------------------------------- 1 | 2 | C:\Users\rdavis\Source\Repos\linqpad-dump-editable\src\DumpEditable\bin\Debug\net47\LINQPad.DumpEditable.dll 3 | LINQPad.DumpEditable 4 | LINQPad.Controls 5 | System.Drawing 6 | 7 | 8 | var c = new { Pt = 25, R = 150, G = 150, B = 150 } 9 | .DumpEditable(out var editor); 10 | 11 | editor.AddEditorRule( 12 | EditorRule.ForType( 13 | Editors.Slider(0, 255), 14 | true)); 15 | 16 | var dc = new DumpContainer().Dump(); 17 | editor.OnChanged += () => 18 | dc.Content = Util.WithStyle("LABEL", 19 | $"font-size:{c.Pt}px;" + 20 | $"color:#{Color.FromArgb(255, c.R, c.G, c.B).Name.Substring(2)};"); 21 | 22 | editor.OnChanged(); -------------------------------------------------------------------------------- /src/DumpEditable/DumpEditable.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net47;netcoreapp3.0 5 | LINQPad.DumpEditable 6 | LINQPad.DumpEditable 7 | true 8 | false 9 | 0.0.1 10 | Ryan Davis 11 | DumpEditable.LINQPad 12 | Extensible inline editor output for LINQPad 5 / LINQPad 6. 13 | (c) Ryan Davis 2019 14 | 15 | https://github.com/rdavisau/linqpad-dump-editable 16 | https://github.com/rdavisau/linqpad-dump-editable 17 | LINQPad, Dump, editor, linqpad-samples 18 | DumpEditable.LINQPad 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DumpEditable/EditableDumpContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reactive.Disposables; 7 | using System.Reflection; 8 | using LINQPad.DumpEditable.Helpers; 9 | using LINQPad.DumpEditable.Models; 10 | 11 | namespace LINQPad.DumpEditable 12 | { 13 | public partial class EditableDumpContainer : DumpContainer 14 | { 15 | private readonly object _obj; 16 | private readonly Dictionary> _changeHandlers 17 | = new Dictionary>(); 18 | 19 | private readonly List _editorRules = new List(); 20 | 21 | public Action OnChanged { get; set; } 22 | public Action OnPropertyValueChanged { get; set; } 23 | public IDisposable KeepRunningToken { get; private set; } 24 | 25 | public void AddChangeHandler(Expression> selector, 26 | Action onChangedAction) 27 | { 28 | var pi = (selector.Body as MemberExpression)?.Member as PropertyInfo; 29 | if (pi is null) 30 | throw new Exception($"Invalid expression passed to {nameof(AddChangeHandler)}"); 31 | 32 | _changeHandlers[pi] = (obj, val) => onChangedAction(obj, (U) val); 33 | } 34 | 35 | public void AddEditorRule(EditorRule rule) 36 | { 37 | _editorRules.Insert(0, rule); 38 | SetContent(); 39 | } 40 | 41 | public EditableDumpContainer(T obj) 42 | { 43 | if (obj.GetType().GetArrayLikeElementType() != null) 44 | throw new Exception("You must Dump enumerable-like objects with the DumpEnumerable overload."); 45 | 46 | if (EditableDumpContainer.DefaultOptions.AutomaticallyKeepQueryRunning) 47 | { 48 | KeepRunningToken = Util.KeepRunning(); 49 | EditableDumpContainer.KeepRunningTokens.Add(KeepRunningToken); 50 | } 51 | 52 | _obj = obj; 53 | 54 | SetContent(); 55 | } 56 | 57 | 58 | public EditableDumpContainer(IEnumerable obj) 59 | { 60 | if (EditableDumpContainer.DefaultOptions.AutomaticallyKeepQueryRunning) 61 | { 62 | KeepRunningToken = Util.KeepRunning(); 63 | EditableDumpContainer.KeepRunningTokens.Add(KeepRunningToken); 64 | } 65 | 66 | _obj = obj; 67 | 68 | SetContent(); 69 | } 70 | 71 | private void SetContent() 72 | { 73 | object content = _obj; 74 | 75 | var isEnumerable = content.GetType().GetArrayLikeElementType() != null; 76 | 77 | try 78 | { 79 | content = 80 | !isEnumerable 81 | ? GetObjectEditorRepresentation(_obj) 82 | : (_obj as IEnumerable).OfType().Select(GetObjectEditorRepresentation).ToList(); 83 | } 84 | catch 85 | { 86 | if (!EditableDumpContainer.DefaultOptions.FailSilently) 87 | throw; 88 | } 89 | 90 | Content = content; 91 | } 92 | 93 | private object GetObjectEditorRepresentation(object input) 94 | { 95 | var properties = input 96 | .GetType() 97 | .GetProperties() 98 | .Select(p => 99 | new PropertyEditor 100 | { 101 | Property = p.Name, 102 | Value = GetPropertyEditor(input, p) 103 | }) 104 | .ToList(); 105 | 106 | return GetDynamicEditorTypeForObject(input, properties); 107 | } 108 | 109 | private readonly Dictionary _dynamicTypeMappings = new Dictionary(); 110 | private object GetDynamicEditorTypeForObject(object input, List propertyEditors) 111 | { 112 | var inType = input.GetType(); 113 | 114 | if (!_dynamicTypeMappings.TryGetValue(inType, out var outType)) 115 | { 116 | outType = DynamicTypeBuilder.CreateTypeForEditor(input, propertyEditors); 117 | _dynamicTypeMappings[inType] = outType; 118 | } 119 | 120 | var @out = Activator.CreateInstance(outType); 121 | var props = outType.GetProperties().ToDictionary(p => p.Name); 122 | foreach (var pe in propertyEditors) 123 | { 124 | var p = props[pe.Property]; 125 | p.SetValue(@out, pe.Value); 126 | } 127 | 128 | return @out; 129 | } 130 | 131 | private object GetPropertyEditor(object o, PropertyInfo p) 132 | { 133 | var allRules = Enumerable.Concat(_editorRules, EditableDumpContainer.GlobalEditorRules); 134 | 135 | foreach (var editor in allRules) 136 | if (editor.Match(o, p)) 137 | return editor.Editor(o, p, () => p.GetValue(o), (v) => 138 | { 139 | SetValue(o, p, v); 140 | 141 | if (!editor.DisableAutomaticRefresh) 142 | SetContent(); 143 | 144 | var newVal = p.GetValue(o); 145 | 146 | if (_changeHandlers.TryGetValue(p, out var handler)) 147 | handler.Invoke((T)o,newVal); 148 | 149 | OnPropertyValueChanged?.Invoke((T)o, p, newVal); 150 | 151 | OnChanged?.Invoke(); 152 | }); 153 | 154 | return p.GetValue(o); 155 | } 156 | 157 | public static void SetValue(object o, PropertyInfo p, object v) 158 | { 159 | if (o.GetType().IsAnonymousType()) 160 | AnonymousObjectMutator.Set(o, p, v); 161 | else 162 | p.SetValue(o, v); 163 | } 164 | 165 | public new void Refresh() 166 | { 167 | SetContent(); 168 | base.Refresh(); 169 | } 170 | } 171 | 172 | public static class EditableDumpContainer 173 | { 174 | public static readonly CompositeDisposable KeepRunningTokens = new CompositeDisposable(); 175 | public static DumpEditableOptions DefaultOptions = DumpEditableOptions.Defaults; 176 | public static EditableDumpContainer For(T obj) 177 | => new EditableDumpContainer(obj); 178 | 179 | public static EditableDumpContainer ForEnumerable(IEnumerable obj) 180 | => new EditableDumpContainer(obj); 181 | 182 | public static void AddGlobalEditorRule(EditorRule rule) 183 | => GlobalEditorRules.Insert(0, rule); 184 | 185 | internal static readonly List GlobalEditorRules = GetDefaultEditors(); 186 | private static List GetDefaultEditors() => 187 | new List 188 | { 189 | EditorRule.ForEnums(), 190 | EditorRule.ForBool(), 191 | EditorRule.ForTypeWithStringBasedEditor(int.TryParse), 192 | EditorRule.ForTypeWithStringBasedEditor(uint.TryParse), 193 | EditorRule.ForTypeWithStringBasedEditor(short.TryParse), 194 | EditorRule.ForTypeWithStringBasedEditor(ushort.TryParse), 195 | EditorRule.ForTypeWithStringBasedEditor(double.TryParse), 196 | EditorRule.ForTypeWithStringBasedEditor(decimal.TryParse), 197 | EditorRule.ForTypeWithStringBasedEditor(float.TryParse), 198 | EditorRule.ForTypeWithStringBasedEditor(long.TryParse), 199 | EditorRule.ForTypeWithStringBasedEditor(ulong.TryParse), 200 | EditorRule.ForTypeWithStringBasedEditor(DateTime.TryParse), 201 | EditorRule.ForTypeWithStringBasedEditor(DateTimeOffset.TryParse), 202 | EditorRule.ForTypeWithStringBasedEditor((string input, out string output) => 203 | { 204 | output = input; 205 | return true; 206 | }), 207 | EditorRule.ForTypeWithStringBasedEditor(byte.TryParse), 208 | EditorRule.ForTypeWithStringBasedEditor(sbyte.TryParse), 209 | EditorRule.ForTypeWithStringBasedEditor(char.TryParse), 210 | EditorRule.ForExpansionAttribute(), 211 | EditorRule.ForNestedAnonymousType() 212 | }; 213 | 214 | } 215 | } -------------------------------------------------------------------------------- /src/DumpEditable/EditorRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using LINQPad.DumpEditable.Helpers; 7 | using LINQPad.DumpEditable.Models; 8 | using Microsoft.VisualBasic; 9 | using Newtonsoft.Json; 10 | 11 | namespace LINQPad.DumpEditable 12 | { 13 | public partial class EditorRule 14 | { 15 | public Func Match { get; set; } 16 | public Func, Action, object> Editor { get; set; } 17 | public bool DisableAutomaticRefresh { get; set; } 18 | } 19 | 20 | public partial class EditorRule 21 | { 22 | public static EditorRule For(Func rule, 23 | Func, Action, object> getEditor, bool disableAutomaticRefresh = false) 24 | => new EditorRule 25 | { 26 | Match = rule, 27 | Editor = getEditor, 28 | DisableAutomaticRefresh = disableAutomaticRefresh 29 | }; 30 | 31 | public static EditorRule ForType(Func, Action, object> getEditor, bool disableAutomaticRefresh = false) 32 | => new EditorRule 33 | { 34 | Match = (o, info) => info.PropertyType == typeof(T), 35 | Editor = getEditor, 36 | DisableAutomaticRefresh = disableAutomaticRefresh 37 | }; 38 | 39 | public static EditorRule ForExpansion(Func rule) 40 | => EditorRule.For( 41 | rule, 42 | (o, p, get, set) => 43 | { 44 | var v = get(); 45 | var isEnumerable = v.GetType().GetArrayLikeElementType() != null; 46 | 47 | var editor = isEnumerable 48 | ? EditableDumpContainer.ForEnumerable(((IEnumerable)v).OfType()) 49 | : EditableDumpContainer.For(v); 50 | 51 | editor.OnChanged += () => set(v); 52 | return editor; 53 | }); 54 | 55 | public static EditorRule ForExpansionAttribute() 56 | => EditorRule.ForExpansion((_, p) => p.GetCustomAttributes().Any()); 57 | 58 | public static EditorRule ForNestedAnonymousType() 59 | => EditorRule.ForExpansion((_, p) => 60 | p.PropertyType.IsAnonymousType() 61 | || (p.PropertyType.GetArrayLikeElementType()?.IsAnonymousType() ?? false)); 62 | 63 | public static EditorRule ForEnums() => 64 | EditorRule.For( 65 | (_, p) => p.PropertyType.IsEnum || p.PropertyType.IsNullableEnum(), 66 | (o, p, get, set) => 67 | { 68 | var isNullable = p.PropertyType.IsNullableEnum(); 69 | var type = isNullable 70 | ? Nullable.GetUnderlyingType(p.PropertyType) 71 | : p.PropertyType; 72 | 73 | var options = type.GetEnumValues().OfType().ToList(); 74 | 75 | return EditableDumpContainer.DefaultOptions.OptionsEditor( 76 | options, 77 | isNullable 78 | ? NullableOptionInclusionKind.IncludeAtEnd 79 | : NullableOptionInclusionKind.DontInclude, 80 | null)(o, p, get, set); 81 | }); 82 | 83 | public static EditorRule ForBool() => 84 | EditorRule.For( 85 | (_, p) => p.PropertyType == typeof(bool) || p.PropertyType == typeof(bool?), 86 | (o, p, get, set) => EditableDumpContainer.DefaultOptions.OptionsEditor( 87 | new [] { true, false }.OfType(), 88 | p.PropertyType == typeof(bool?) 89 | ? NullableOptionInclusionKind.IncludeAtEnd 90 | : NullableOptionInclusionKind.DontInclude, 91 | null)(o, p, get, set)); 92 | 93 | public static EditorRule ForTypeWithStringBasedEditor(ParseFunc parseFunc, bool supportNullable = true, bool supportEnumerable = true) 94 | => new EditorRule 95 | { 96 | Match = (o, info) => 97 | info.PropertyType == typeof(T) 98 | || (supportNullable && Nullable.GetUnderlyingType(info.PropertyType) == typeof(T)) 99 | || (supportEnumerable && info.PropertyType.GetArrayLikeElementType() == typeof(T)), 100 | Editor = (o, info, get, set) => EditableDumpContainer.DefaultOptions.StringBasedEditor(WrapParseFunc(parseFunc), supportNullable, supportEnumerable)(o, info, get, set), 101 | DisableAutomaticRefresh = true, 102 | }; 103 | 104 | private static ParseFunc WrapParseFunc(ParseFunc parseFunc) 105 | => (string input, out object output) => 106 | { 107 | var ret = parseFunc(input, out var tOut); 108 | output = tOut; 109 | 110 | return ret; 111 | }; 112 | 113 | public delegate V ParseFunc(T input, out U output); 114 | 115 | public const string NullString = "(null)"; 116 | public const string EmptyString = "(empty string)"; 117 | } 118 | } -------------------------------------------------------------------------------- /src/DumpEditable/Editors.TextBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace LINQPad.DumpEditable 6 | { 7 | public static partial class Editors 8 | { 9 | private const string DefaultWidth = "15em"; 10 | private const string ByteWidth = "3em"; 11 | private const string Int16Width = "5em"; 12 | private const string Int32Width = "7em"; 13 | private const string NumericWidth = "9em"; 14 | private const string DateTimeWidth = "13.5em"; 15 | private const string DateTimeOffsetWidth = "17em"; 16 | 17 | public static readonly Dictionary TextBoxTypeWidths = new Dictionary 18 | { 19 | [typeof(byte)] = ByteWidth, 20 | [typeof(Int16)] = Int16Width, [typeof(UInt16)] = Int16Width, 21 | [typeof(Int32)] = Int32Width, [typeof(UInt32)] = Int32Width, 22 | [typeof(Int64)] = NumericWidth, [typeof(UInt64)] = NumericWidth, 23 | [typeof(double)] = NumericWidth, 24 | [typeof(float)] = NumericWidth, 25 | [typeof(decimal)] = NumericWidth, 26 | [typeof(DateTime)] = DateTimeWidth, [typeof(DateTimeOffset)] = DateTimeOffsetWidth 27 | }; 28 | 29 | public static Func WidthForTextBox { get; set; } = (o, p) => 30 | { 31 | var type = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType; 32 | 33 | return TextBoxTypeWidths.TryGetValue(type, out var width) 34 | ? width 35 | : DefaultWidth; 36 | }; 37 | } 38 | } -------------------------------------------------------------------------------- /src/DumpEditable/Editors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using LINQPad.Controls; 7 | using LINQPad.DumpEditable.Helpers; 8 | using LINQPad.DumpEditable.Models; 9 | using Microsoft.VisualBasic; 10 | using Newtonsoft.Json; 11 | 12 | namespace LINQPad.DumpEditable 13 | { 14 | public static partial class Editors 15 | { 16 | public static Func, Action, object> Slider(int min, int max) 17 | => Slider(min, max, x => x, x => x); 18 | 19 | public static Func, Action, object> Slider( 20 | T min, T max, 21 | Func toInt, 22 | Func fromInt) 23 | => (o, p, gv, sv) => 24 | { 25 | var v = gv(); 26 | var vc = new DumpContainer { Content = v, Style = "min-width: 30px" }; 27 | var s = new RangeControl( 28 | Convert.ToInt32(toInt(min)), 29 | Convert.ToInt32(toInt(max)), 30 | Convert.ToInt32(toInt((T)v))) 31 | { IsMultithreaded = true }; 32 | 33 | s.ValueInput += delegate 34 | { 35 | var val = fromInt(s.Value); 36 | sv(val); 37 | vc.Content = val; 38 | }; 39 | 40 | var config = new {Min = min, Max = max}; 41 | var editor = EditableDumpContainer.For(config); 42 | editor.AddChangeHandler(x => x.Min, (_, m) => s.Min = Convert.ToInt32(toInt(m))); 43 | editor.AddChangeHandler(x => x.Max, (_, m) => s.Max = Convert.ToInt32(toInt(m))); 44 | 45 | var editorDc = new DumpContainer { Content = editor }; 46 | var display = true; 47 | var toggleEditor = new Hyperlinq(() => 48 | { 49 | display = !display; 50 | editorDc.Style = display ? "" : "display:none"; 51 | }, "[*]"); 52 | toggleEditor.Action(); 53 | 54 | return Util.VerticalRun(vc, Util.HorizontalRun(false, s, toggleEditor), editorDc); 55 | }; 56 | 57 | public static Func, Action, object> ChoicesWithRadioButtons( 58 | IEnumerable choices, NullableOptionInclusionKind nullKind, Func toString = null) => 59 | ChoicesWithRadioButtons(choices.OfType(), nullKind, o => toString((T) o)); 60 | 61 | public static Func, Action, object> ChoicesWithRadioButtons( 62 | IEnumerable choices, NullableOptionInclusionKind nullKind, Func toString = null) => 63 | (o, p, gv, sv) => 64 | { 65 | var group = Guid.NewGuid().ToString(); 66 | var v = gv(); 67 | 68 | var radioButtons = 69 | choices 70 | .Select(x => new RadioButton(@group, toString?.Invoke(x) ?? $"{x}", x.Equals(v), b => sv(x))) 71 | .ToList(); 72 | 73 | var nullOption = new RadioButton(@group, NullString, v == null, _ => sv(null)); 74 | switch (nullKind) 75 | { 76 | case NullableOptionInclusionKind.IncludeAtStart: 77 | radioButtons.Insert(0, nullOption); 78 | break; 79 | 80 | case NullableOptionInclusionKind.IncludeAtEnd: 81 | radioButtons.Add(nullOption); 82 | break; 83 | } 84 | 85 | return Util.HorizontalRun((bool)true, (IEnumerable)radioButtons); 86 | }; 87 | 88 | public static Func, Action, object> ChoicesWithHyperlinqs( 89 | IEnumerable choices, NullableOptionInclusionKind nullKind, Func toString = null) => 90 | (o, p, gv, sv) => 91 | { 92 | var preceding = new object[] {gv(), "["}; 93 | var trailing = new object[] {"]"}; 94 | 95 | var values = choices.Select(x => new Hyperlinq(() => sv(x), toString?.Invoke(x) ?? $"{x}")).ToList(); 96 | 97 | var nullOption = new Hyperlinq(() => sv(null), NullString); 98 | 99 | switch (nullKind) 100 | { 101 | case NullableOptionInclusionKind.IncludeAtStart: 102 | values.Insert(0, nullOption); 103 | break; 104 | 105 | case NullableOptionInclusionKind.IncludeAtEnd: 106 | values.Add(nullOption); 107 | break; 108 | } 109 | 110 | return Util.HorizontalRun( 111 | true, 112 | Enumerable.Concat( 113 | new object[] {gv(), "["}, 114 | choices.Select(x => new Hyperlinq(() => sv(x), toString?.Invoke(x) ?? $"{x}"))) 115 | .Concat(new[] {"]"})); 116 | }; 117 | 118 | public static Func, bool, bool, Func, Action, object>> TextBoxBasedStringEditor 119 | (bool liveUpdates) => (parse, nullable, enumerable) => (o, p, gv, sv) => 120 | Editors.StringWithTextBox(o, p, gv, sv, parse, nullable, enumerable, liveUpdates); 121 | 122 | internal static object StringWithTextBox(object o, PropertyInfo p, Func gv, Action sv, 123 | EditorRule.ParseFunc parseFunc, 124 | bool supportNullable = true, bool supportEnumerable = true, bool liveUpdate = true) 125 | => StringWithTextBox(o, p, gv, sv, (string input, out object output) => 126 | { 127 | var ret = parseFunc(input, out var outT); 128 | output = outT; 129 | 130 | return ret; 131 | }, supportNullable, supportEnumerable, liveUpdate); 132 | 133 | public static object StringWithTextBox(object o, PropertyInfo p, Func gv, Action sv, EditorRule.ParseFunc parseFunc, 134 | bool supportNullable = true, bool supportEnumerable = true, bool liveUpdate = true) 135 | { 136 | var type = p.PropertyType; 137 | var isEnumerable = supportEnumerable && type.GetArrayLikeElementType() != null; 138 | 139 | // handle string which is IEnumerable 140 | if (type == typeof(string) && type.GetArrayLikeElementType() == typeof(char)) 141 | isEnumerable = false; 142 | 143 | string GetStringRepresentationForValue() 144 | { 145 | var v = gv(); 146 | 147 | var desc = v == null 148 | ? NullString 149 | : (isEnumerable ? JsonConvert.SerializeObject(v) : $"{v}"); 150 | 151 | // hyperlinq doesn't like empty strings 152 | if (desc == String.Empty) 153 | desc = EmptyString; 154 | 155 | return desc; 156 | } 157 | 158 | bool TryGetParsedValue(string str, out object @out) 159 | { 160 | var canConvert = parseFunc(str, out var output); 161 | if (isEnumerable) 162 | { 163 | try 164 | { 165 | var val = JsonConvert.DeserializeObject(str, type); 166 | @out = val; 167 | return true; 168 | } 169 | catch 170 | { 171 | @out = null; 172 | return false; // can't deserialise 173 | } 174 | } 175 | 176 | if (canConvert) 177 | { 178 | @out = output; 179 | return true; 180 | } 181 | 182 | if (supportNullable && (str == String.Empty)) 183 | { 184 | @out = null; 185 | return true; 186 | } 187 | 188 | @out = null; 189 | return false; // can't convert 190 | } 191 | 192 | var updateButton = new Button("update") { Visible = false }; 193 | 194 | Action onText = t => 195 | { 196 | var canParse = TryGetParsedValue(t.Text, out var newValue); 197 | 198 | if (liveUpdate && canParse) 199 | sv(newValue); 200 | else 201 | updateButton.Visible = canParse; 202 | }; 203 | 204 | var initialText = GetStringRepresentationForValue() ?? ""; 205 | var s = !isEnumerable 206 | ? (ITextControl) new TextBox(initialText, WidthForTextBox(o,p), onText) { IsMultithreaded = true } 207 | : (ITextControl) new TextArea(initialText, 40, onText) { IsMultithreaded = true }; 208 | 209 | updateButton.Click += (sender, e) => 210 | { 211 | if (!TryGetParsedValue(s.Text, out var newValue)) return; 212 | 213 | sv(newValue); 214 | updateButton.Visible = false; 215 | }; 216 | 217 | var dc = new DumpContainer 218 | { 219 | Style = "text-align: center; vertical-align: middle;", 220 | Content = Util.HorizontalRun(true, s, updateButton) 221 | }; 222 | 223 | return dc; 224 | } 225 | 226 | public const string NullString = "(null)"; 227 | public const string EmptyString = "(empty string)"; 228 | } 229 | } -------------------------------------------------------------------------------- /src/DumpEditable/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LINQPad.DumpEditable 8 | { 9 | public static class Extensions 10 | { 11 | public static T DumpEditable(this T obj) 12 | => DumpEditable(obj, out _); 13 | 14 | public static T DumpEditable(this T obj, out EditableDumpContainer container) 15 | { 16 | container = new EditableDumpContainer(obj); 17 | container.Dump(); 18 | 19 | return obj; 20 | } 21 | 22 | public static IEnumerable DumpEditableEnumerable(this IEnumerable obj) 23 | => DumpEditableEnumerable(obj, out _); 24 | 25 | public static IEnumerable DumpEditableEnumerable(this IEnumerable obj, out EditableDumpContainer container) 26 | { 27 | container = new EditableDumpContainer(obj); 28 | container.Dump(); 29 | 30 | return obj; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/DumpEditable/Helpers/AnonymousObjectMutator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace LINQPad.DumpEditable.Helpers 11 | { 12 | // Adapted from https://stackoverflow.com/a/30242237/752273 13 | // (the "more elaborate" version - here: https://ideone.com/ALG9DE) 14 | internal static class AnonymousObjectMutator 15 | { 16 | private const BindingFlags FieldFlags = BindingFlags.NonPublic | BindingFlags.Instance; 17 | private const BindingFlags PropFlags = BindingFlags.Public | BindingFlags.Instance; 18 | private static readonly string[] BackingFieldFormats = { "<{0}>i__Field", "<{0}>" }; 19 | private static ConcurrentDictionary>> _map = 20 | new ConcurrentDictionary>>(); 21 | 22 | public static T Set( 23 | this T instance, 24 | Expression> propExpression, 25 | TProperty newValue) where T : class 26 | { 27 | GetSetterFor(propExpression)(instance, newValue); 28 | return instance; 29 | } 30 | 31 | public static void Set( 32 | this object instance, 33 | PropertyInfo p, 34 | object newValue) 35 | { 36 | GetSetterFor(instance.GetType(), p)(instance, newValue); 37 | } 38 | 39 | private static Action GetSetterFor(Type t, PropertyInfo property) 40 | { 41 | Action setter = null; 42 | GetPropMap(t).TryGetValue(property.Name, out setter); 43 | if (setter == null) 44 | throw new InvalidOperationException("No setter found"); 45 | return setter; 46 | } 47 | 48 | private static Action GetSetterFor(Expression> propExpression) 49 | { 50 | var memberExpression = propExpression.Body as MemberExpression; 51 | if (memberExpression == null || memberExpression.Member.MemberType != MemberTypes.Property) 52 | throw new InvalidOperationException("Only property expressions are supported"); 53 | Action setter = null; 54 | GetPropMap().TryGetValue(memberExpression.Member.Name, out setter); 55 | if (setter == null) 56 | throw new InvalidOperationException("No setter found"); 57 | return setter; 58 | } 59 | 60 | private static IDictionary> GetPropMap() 61 | => GetPropMap(typeof(T)); 62 | 63 | private static IDictionary> GetPropMap(Type t) 64 | => _map.GetOrAdd(t, x => BuildPropMap(t)); 65 | 66 | private static IDictionary> BuildPropMap(Type t) 67 | { 68 | var typeMap = new Dictionary>(); 69 | var fields = t.GetFields(FieldFlags); 70 | foreach (var pi in t.GetProperties(PropFlags)) 71 | { 72 | var backingFieldNames = BackingFieldFormats.Select(x => string.Format(x, pi.Name)).ToList(); 73 | var fi = fields.FirstOrDefault(f => backingFieldNames.Contains(f.Name) && f.FieldType == pi.PropertyType); 74 | if (fi == null) 75 | throw new NotSupportedException(string.Format("No backing field found for property {0}.", pi.Name)); 76 | typeMap.Add(pi.Name, (inst, val) => fi.SetValue(inst, val)); 77 | } 78 | return typeMap; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/DumpEditable/Helpers/CompositeDisposable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. 2 | // https://github.com/mono/rx/blob/master/Rx/NET/Source/System.Reactive.Core/Reactive/Disposables/CompositeDisposable.cs 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace System.Reactive.Disposables 7 | { 8 | /// 9 | /// Represents a group of disposable resources that are disposed together. 10 | /// 11 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", 12 | Justification = "Backward compat + ideally want to get rid of the ICollection nature of the type.")] 13 | public sealed class CompositeDisposable : ICollection 14 | { 15 | private readonly object _gate = new object(); 16 | 17 | private bool _disposed; 18 | private List _disposables; 19 | private int _count; 20 | private const int SHRINK_THRESHOLD = 64; 21 | 22 | /// 23 | /// Initializes a new instance of the class with no disposables contained by it initially. 24 | /// 25 | public CompositeDisposable() 26 | { 27 | _disposables = new List(); 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class with the specified number of disposables. 32 | /// 33 | /// The number of disposables that the new CompositeDisposable can initially store. 34 | /// is less than zero. 35 | public CompositeDisposable(int capacity) 36 | { 37 | if (capacity < 0) 38 | throw new ArgumentOutOfRangeException("capacity"); 39 | 40 | _disposables = new List(capacity); 41 | } 42 | 43 | /// 44 | /// Initializes a new instance of the class from a group of disposables. 45 | /// 46 | /// Disposables that will be disposed together. 47 | /// is null. 48 | public CompositeDisposable(params IDisposable[] disposables) 49 | { 50 | if (disposables == null) 51 | throw new ArgumentNullException("disposables"); 52 | 53 | _disposables = new List(disposables); 54 | _count = _disposables.Count; 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the class from a group of disposables. 59 | /// 60 | /// Disposables that will be disposed together. 61 | /// is null. 62 | public CompositeDisposable(IEnumerable disposables) 63 | { 64 | if (disposables == null) 65 | throw new ArgumentNullException("disposables"); 66 | 67 | _disposables = new List(disposables); 68 | _count = _disposables.Count; 69 | } 70 | 71 | /// 72 | /// Gets the number of disposables contained in the CompositeDisposable. 73 | /// 74 | public int Count 75 | { 76 | get { return _count; } 77 | } 78 | 79 | /// 80 | /// Adds a disposable to the CompositeDisposable or disposes the disposable if the CompositeDisposable is disposed. 81 | /// 82 | /// Disposable to add. 83 | /// is null. 84 | public void Add(IDisposable item) 85 | { 86 | if (item == null) 87 | throw new ArgumentNullException("item"); 88 | 89 | var shouldDispose = false; 90 | lock (_gate) 91 | { 92 | shouldDispose = _disposed; 93 | if (!_disposed) 94 | { 95 | _disposables.Add(item); 96 | _count++; 97 | } 98 | } 99 | 100 | if (shouldDispose) 101 | item.Dispose(); 102 | } 103 | 104 | /// 105 | /// Removes and disposes the first occurrence of a disposable from the CompositeDisposable. 106 | /// 107 | /// Disposable to remove. 108 | /// true if found; false otherwise. 109 | /// is null. 110 | public bool Remove(IDisposable item) 111 | { 112 | if (item == null) 113 | throw new ArgumentNullException("item"); 114 | 115 | var shouldDispose = false; 116 | 117 | lock (_gate) 118 | { 119 | if (!_disposed) 120 | { 121 | // 122 | // List doesn't shrink the size of the underlying array but does collapse the array 123 | // by copying the tail one position to the left of the removal index. We don't need 124 | // index-based lookup but only ordering for sequential disposal. So, instead of spending 125 | // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also 126 | // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. 127 | // 128 | var i = _disposables.IndexOf(item); 129 | if (i >= 0) 130 | { 131 | shouldDispose = true; 132 | _disposables[i] = null; 133 | _count--; 134 | 135 | if (_disposables.Capacity > SHRINK_THRESHOLD && _count < _disposables.Capacity / 2) 136 | { 137 | var old = _disposables; 138 | _disposables = new List(_disposables.Capacity / 2); 139 | 140 | foreach (var d in old) 141 | if (d != null) 142 | _disposables.Add(d); 143 | } 144 | } 145 | } 146 | } 147 | 148 | if (shouldDispose) 149 | item.Dispose(); 150 | 151 | return shouldDispose; 152 | } 153 | 154 | /// 155 | /// Disposes all disposables in the group and removes them from the group. 156 | /// 157 | public void Dispose() 158 | { 159 | var currentDisposables = default(IDisposable[]); 160 | lock (_gate) 161 | { 162 | if (!_disposed) 163 | { 164 | _disposed = true; 165 | currentDisposables = _disposables.ToArray(); 166 | _disposables.Clear(); 167 | _count = 0; 168 | } 169 | } 170 | 171 | if (currentDisposables != null) 172 | { 173 | foreach (var d in currentDisposables) 174 | if (d != null) 175 | d.Dispose(); 176 | } 177 | } 178 | 179 | /// 180 | /// Removes and disposes all disposables from the CompositeDisposable, but does not dispose the CompositeDisposable. 181 | /// 182 | public void Clear() 183 | { 184 | var currentDisposables = default(IDisposable[]); 185 | lock (_gate) 186 | { 187 | currentDisposables = _disposables.ToArray(); 188 | _disposables.Clear(); 189 | _count = 0; 190 | } 191 | 192 | foreach (var d in currentDisposables) 193 | if (d != null) 194 | d.Dispose(); 195 | } 196 | 197 | /// 198 | /// Determines whether the CompositeDisposable contains a specific disposable. 199 | /// 200 | /// Disposable to search for. 201 | /// true if the disposable was found; otherwise, false. 202 | /// is null. 203 | public bool Contains(IDisposable item) 204 | { 205 | if (item == null) 206 | throw new ArgumentNullException("item"); 207 | 208 | lock (_gate) 209 | { 210 | return _disposables.Contains(item); 211 | } 212 | } 213 | 214 | /// 215 | /// Copies the disposables contained in the CompositeDisposable to an array, starting at a particular array index. 216 | /// 217 | /// Array to copy the contained disposables to. 218 | /// Target index at which to copy the first disposable of the group. 219 | /// is null. 220 | /// is less than zero. -or - is larger than or equal to the array length. 221 | public void CopyTo(IDisposable[] array, int arrayIndex) 222 | { 223 | if (array == null) 224 | throw new ArgumentNullException("array"); 225 | if (arrayIndex < 0 || arrayIndex >= array.Length) 226 | throw new ArgumentOutOfRangeException("arrayIndex"); 227 | 228 | lock (_gate) 229 | { 230 | Array.Copy(_disposables.Where(d => d != null).ToArray(), 0, array, arrayIndex, 231 | array.Length - arrayIndex); 232 | } 233 | } 234 | 235 | /// 236 | /// Always returns false. 237 | /// 238 | public bool IsReadOnly 239 | { 240 | get { return false; } 241 | } 242 | 243 | /// 244 | /// Returns an enumerator that iterates through the CompositeDisposable. 245 | /// 246 | /// An enumerator to iterate over the disposables. 247 | public IEnumerator GetEnumerator() 248 | { 249 | var res = default(IEnumerable); 250 | 251 | lock (_gate) 252 | { 253 | res = _disposables.Where(d => d != null).ToList(); 254 | } 255 | 256 | return res.GetEnumerator(); 257 | } 258 | 259 | /// 260 | /// Returns an enumerator that iterates through the CompositeDisposable. 261 | /// 262 | /// An enumerator to iterate over the disposables. 263 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 264 | { 265 | return GetEnumerator(); 266 | } 267 | 268 | /// 269 | /// Gets a value that indicates whether the object is disposed. 270 | /// 271 | public bool IsDisposed 272 | { 273 | get { return _disposed; } 274 | } 275 | } 276 | } -------------------------------------------------------------------------------- /src/DumpEditable/Helpers/DynamicTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using LINQPad.DumpEditable.Models; 9 | 10 | namespace LINQPad.DumpEditable.Helpers 11 | { 12 | // adapted from https://stackoverflow.com/a/3862241/752273 13 | public static class DynamicTypeBuilder 14 | { 15 | private const string AnonymousTypeIndicator = "<>f__AnonymousType"; 16 | private const string AnonymousTypeReplacement = "ø"; 17 | 18 | public static Type CreateTypeForEditor(object source, List properties) 19 | { 20 | var tb = GetTypeBuilder(source); 21 | var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName); 22 | 23 | foreach (var field in properties) 24 | CreateProperty(tb, field.Property, typeof(object)); 25 | 26 | return tb.CreateType(); 27 | } 28 | 29 | private static TypeBuilder GetTypeBuilder(object source) 30 | { 31 | var type = source.GetType(); 32 | var typeSignature = !String.IsNullOrWhiteSpace(type.Namespace) 33 | ? $"{type.Namespace}.{type.Name}" 34 | : type.Name; 35 | 36 | if (typeSignature.StartsWith(AnonymousTypeIndicator)) 37 | typeSignature = AnonymousTypeReplacement; 38 | 39 | var an = new AssemblyName(typeSignature); 40 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run); 41 | var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); 42 | var tb = moduleBuilder.DefineType(typeSignature, 43 | TypeAttributes.Public | 44 | TypeAttributes.Class | 45 | TypeAttributes.AutoClass | 46 | TypeAttributes.AnsiClass | 47 | TypeAttributes.BeforeFieldInit | 48 | TypeAttributes.AutoLayout, 49 | null); 50 | 51 | return tb; 52 | } 53 | 54 | private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType) 55 | { 56 | var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); 57 | 58 | var propertyBuilder = tb.DefineProperty(propertyName, System.Reflection.PropertyAttributes.HasDefault, propertyType, null); 59 | var getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes); 60 | var getIl = getPropMthdBldr.GetILGenerator(); 61 | 62 | getIl.Emit(OpCodes.Ldarg_0); 63 | getIl.Emit(OpCodes.Ldfld, fieldBuilder); 64 | getIl.Emit(OpCodes.Ret); 65 | 66 | var setPropMthdBldr = 67 | tb.DefineMethod("set_" + propertyName, 68 | MethodAttributes.Public | 69 | MethodAttributes.SpecialName | 70 | MethodAttributes.HideBySig, 71 | null, new[] { propertyType }); 72 | 73 | var setIl = setPropMthdBldr.GetILGenerator(); 74 | var modifyProperty = setIl.DefineLabel(); 75 | var exitSet = setIl.DefineLabel(); 76 | 77 | setIl.MarkLabel(modifyProperty); 78 | setIl.Emit(OpCodes.Ldarg_0); 79 | setIl.Emit(OpCodes.Ldarg_1); 80 | setIl.Emit(OpCodes.Stfld, fieldBuilder); 81 | 82 | setIl.Emit(OpCodes.Nop); 83 | setIl.MarkLabel(exitSet); 84 | setIl.Emit(OpCodes.Ret); 85 | 86 | propertyBuilder.SetGetMethod(getPropMthdBldr); 87 | propertyBuilder.SetSetMethod(setPropMthdBldr); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DumpEditable/Helpers/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace LINQPad.DumpEditable.Helpers 7 | { 8 | public static class TypeExtensions 9 | { 10 | public static bool IsAnonymousType(this Type type) => 11 | type.FullName.Contains("AnonymousType") // name contains Anonymous Type 12 | && type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Count() > 0; 13 | 14 | 15 | // https://stackoverflow.com/a/17713382/752273 16 | public static Type GetArrayLikeElementType(this Type type) 17 | { 18 | // Type is Array 19 | // short-circuit if you expect lots of arrays 20 | if (type.IsArray) 21 | return type.GetElementType(); 22 | 23 | // type is IEnumerable; 24 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 25 | return type.GetGenericArguments()[0]; 26 | 27 | // type implements/extends IEnumerable; 28 | var enumType = type.GetInterfaces() 29 | .Where(t => t.IsGenericType && 30 | t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 31 | .Select(t => t.GenericTypeArguments[0]).FirstOrDefault(); 32 | return enumType; 33 | } 34 | 35 | public static bool IsNullableEnum(this Type t) 36 | => (Nullable.GetUnderlyingType(t)?.IsEnum ?? false); 37 | 38 | public static bool IsNumeric(this Type t) 39 | => NumericTypes.Contains(t); 40 | 41 | private static readonly Type[] NumericTypes = 42 | { 43 | typeof(int), typeof(uint), 44 | typeof(short), typeof(ushort), 45 | typeof(long), typeof(ulong), 46 | typeof(double), typeof(float), 47 | typeof(decimal) 48 | }; 49 | } 50 | } -------------------------------------------------------------------------------- /src/DumpEditable/Models/DumpEditableExpandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LINQPad.DumpEditable.Models 8 | { 9 | public class DumpEditableExpandAttribute : Attribute 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/DumpEditable/Models/DumpEditableOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace LINQPad.DumpEditable.Models 9 | { 10 | public class DumpEditableOptions 11 | { 12 | public static DumpEditableOptions Defaults => new DumpEditableOptions 13 | { 14 | AutomaticallyKeepQueryRunning = true, 15 | FailSilently = false, 16 | OptionsEditor = Editors.ChoicesWithRadioButtons, 17 | StringBasedEditor = Editors.TextBoxBasedStringEditor(false), 18 | }; 19 | 20 | public bool AutomaticallyKeepQueryRunning { get; set; } 21 | public bool FailSilently { get; set; } 22 | 23 | public Func, NullableOptionInclusionKind, Func, Func, Action, object>> OptionsEditor; 24 | public Func, bool, bool, Func, Action, object>> StringBasedEditor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DumpEditable/Models/NullableOptionInclusionKind.cs: -------------------------------------------------------------------------------- 1 | namespace LINQPad.DumpEditable.Models 2 | { 3 | public enum NullableOptionInclusionKind 4 | { 5 | DontInclude, 6 | IncludeAtStart, 7 | IncludeAtEnd 8 | } 9 | } -------------------------------------------------------------------------------- /src/DumpEditable/Models/PropertyEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LINQPad.DumpEditable.Models 8 | { 9 | public class PropertyEditor 10 | { 11 | public string Property { get; set; } 12 | public object Value { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/linqpad-dump-editable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28729.10 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DumpEditable", "DumpEditable\DumpEditable.csproj", "{67C4F4C2-21AB-4A15-8419-6F563A3DE873}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {67C4F4C2-21AB-4A15-8419-6F563A3DE873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {67C4F4C2-21AB-4A15-8419-6F563A3DE873}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {67C4F4C2-21AB-4A15-8419-6F563A3DE873}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {67C4F4C2-21AB-4A15-8419-6F563A3DE873}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {DD4C4D88-1121-4B0C-BC54-6CE368E2C92A} 24 | EndGlobalSection 25 | EndGlobal 26 | --------------------------------------------------------------------------------