Repository: integrativesoft/lara Branch: master Commit: 454d0d0583ca Files: 272 Total size: 781.3 KB Directory structure: gitextract_6r9zre1z/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md └── src/ ├── Boilerplate/ │ ├── Program.cs │ ├── Wiki/ │ │ ├── ComponentPropertyExample.cs │ │ ├── ComposingComponent.cs │ │ ├── ConditionalRenderComponent.cs │ │ ├── DocumentContextExample.cs │ │ ├── HttpContextExample.cs │ │ ├── LoopComponent.cs │ │ ├── RedBoxExample.cs │ │ ├── SimpleComponent.cs │ │ ├── UserInputComponent.cs │ │ └── UserTextComponent.cs │ └── WikiExamples.csproj ├── LaraClient/ │ ├── .eslintignore │ ├── .eslintrc │ ├── .prettierrc │ ├── LaraClient.njsproj │ ├── README.md │ ├── dist/ │ │ └── lara-client.js.LICENSE.txt │ ├── package.json │ ├── src/ │ │ ├── Autocomplete.ts │ │ ├── Blocker.ts │ │ ├── ContentInterfaces.ts │ │ ├── DeltaInterfaces.ts │ │ ├── Initializer.ts │ │ ├── InputCollector.ts │ │ ├── RegisteredEvents.ts │ │ ├── Sequencer.ts │ │ ├── SocketEvents.ts │ │ ├── Worker.ts │ │ ├── custom.d.ts │ │ └── index.ts │ └── tsconfig.json ├── LaraClient.sln ├── LaraDocumentation/ │ ├── Content/ │ │ └── Welcome.aml │ ├── ContentLayout.content │ └── LaraDocumentation.shfbproj ├── LaraUI/ │ ├── .gitignore │ ├── Autocomplete/ │ │ ├── AutocompleteElement.cs │ │ ├── AutocompleteEntry.cs │ │ ├── AutocompleteOptions.cs │ │ ├── AutocompletePayload.cs │ │ ├── AutocompleteRegistry.cs │ │ ├── AutocompleteResponse.cs │ │ ├── AutocompleteService.cs │ │ └── IAutocompleteProvider.cs │ ├── Components/ │ │ ├── ComponentRegistry.cs │ │ ├── Fragment.cs │ │ ├── LaraWebComponent.cs │ │ ├── RenderIf.cs │ │ ├── Shadow.cs │ │ ├── Slot.cs │ │ ├── SlottedCalculator.cs │ │ ├── WebComponent.cs │ │ └── WebComponentOptions.cs │ ├── DOM/ │ │ ├── Attributes.cs │ │ ├── BlockOptions.cs │ │ ├── ChildrenBindingSubscription.cs │ │ ├── Document.cs │ │ ├── DocumentIdMap.cs │ │ ├── DocumentWriter.cs │ │ ├── DomSurgeon.cs │ │ ├── DuplicateElementIdException.cs │ │ ├── Element.cs │ │ ├── ElementFactory.cs │ │ ├── EventSettings.cs │ │ ├── GlobalSerializer.cs │ │ ├── HtmlReference.cs │ │ ├── MessageRegistry.cs │ │ ├── Node.cs │ │ ├── NodeExtensions.cs │ │ └── TextNode.cs │ ├── Delta/ │ │ ├── AttributeEditedDelta.cs │ │ ├── AttributeRemovedDelta.cs │ │ ├── BaseDelta.cs │ │ ├── ClearChildrenDelta.cs │ │ ├── ContentArrayNode.cs │ │ ├── ContentAttribute.cs │ │ ├── ContentElementNode.cs │ │ ├── ContentNode.cs │ │ ├── ContentPlaceholder.cs │ │ ├── ContentTextNode.cs │ │ ├── ElementValue.cs │ │ ├── EventResult.cs │ │ ├── FocusDelta.cs │ │ ├── NodeAddedDelta.cs │ │ ├── NodeInsertedDelta.cs │ │ ├── NodeLocator.cs │ │ ├── NodeRemovedDelta.cs │ │ ├── PlugOptions.cs │ │ ├── RemoveElementDelta.cs │ │ ├── RenderDelta.cs │ │ ├── ReplaceDelta.cs │ │ ├── ServerEventsDelta.cs │ │ ├── SetCheckedDelta.cs │ │ ├── SetIdDelta.cs │ │ ├── SetValueDelta.cs │ │ ├── SubmitJsDelta.cs │ │ ├── SubscribeDelta.cs │ │ ├── SwapChildrenDelta.cs │ │ ├── TextModifiedDelta.cs │ │ ├── UnRenderDelta.cs │ │ └── UnsubscribeDelta.cs │ ├── Elements/ │ │ ├── GenericElement.cs │ │ ├── HtmlAnchorElement.cs │ │ ├── HtmlBodyElement.cs │ │ ├── HtmlButtonElement.cs │ │ ├── HtmlColGroupElement.cs │ │ ├── HtmlDivElement.cs │ │ ├── HtmlHeadElement.cs │ │ ├── HtmlHeadingElement.cs │ │ ├── HtmlImageElement.cs │ │ ├── HtmlInputElement.cs │ │ ├── HtmlLabelElement.cs │ │ ├── HtmlLiElement.cs │ │ ├── HtmlLinkElement.cs │ │ ├── HtmlMetaElement.cs │ │ ├── HtmlMeterElement.cs │ │ ├── HtmlOlElement.cs │ │ ├── HtmlOptionElement.cs │ │ ├── HtmlOptionGroupElement.cs │ │ ├── HtmlParagraphElement.cs │ │ ├── HtmlScriptElement.cs │ │ ├── HtmlSelectElement.cs │ │ ├── HtmlSpanElement.cs │ │ ├── HtmlTableCellElement.cs │ │ ├── HtmlTableElement.cs │ │ ├── HtmlTableHeaderElement.cs │ │ ├── HtmlTableRowElement.cs │ │ ├── HtmlTableSectionElement.cs │ │ ├── HtmlTextAreaElement.cs │ │ └── HtmlTitleElement.cs │ ├── GlobalSuppressions.cs │ ├── LaraUI.csproj │ ├── LaraUI.csproj.DotSettings │ ├── Main/ │ │ ├── Application.cs │ │ ├── BaseContext.cs │ │ ├── BinaryServiceContent.cs │ │ ├── BinaryServicePublished.cs │ │ ├── Connection.cs │ │ ├── Connections.cs │ │ ├── GlobalConstants.cs │ │ ├── IBinaryService.cs │ │ ├── INavigation.cs │ │ ├── IPage.cs │ │ ├── IPageContext.cs │ │ ├── IPublishedItem.cs │ │ ├── IWebService.cs │ │ ├── IWebServiceContext.cs │ │ ├── JSBridge.cs │ │ ├── LaraBinaryServiceAttribute.cs │ │ ├── LaraPageAttribute.cs │ │ ├── LaraUI.cs │ │ ├── LaraWebServiceAttribute.cs │ │ ├── Navigation.cs │ │ ├── PageContext.cs │ │ ├── PagePublished.cs │ │ ├── Published.cs │ │ ├── Session.cs │ │ ├── SessionStorage.cs │ │ ├── SingleElementPage.cs │ │ ├── StaleConnectionsCollector.cs │ │ ├── StaticContent.cs │ │ ├── TemplateBuilder.cs │ │ ├── WebServiceContent.cs │ │ ├── WebServiceContext.cs │ │ └── WebServicePublished.cs │ ├── Middleware/ │ │ ├── BaseHandler.cs │ │ ├── BrowserAppController.cs │ │ ├── ClientEventMessage.cs │ │ ├── ClientLibraryHandler.cs │ │ ├── ContentTypes.cs │ │ ├── DefaultErrorPage.cs │ │ ├── DiscardHandler.cs │ │ ├── DiscardParameters.cs │ │ ├── ErrorPages.cs │ │ ├── EventParameters.cs │ │ ├── FormFile.cs │ │ ├── FormFileCollection.cs │ │ ├── IModeController.cs │ │ ├── KeepAliveHandler.cs │ │ ├── LaraMiddleware.cs │ │ ├── LocalhostFilter.cs │ │ ├── MiddlewareCommon.cs │ │ ├── NotFoundMiddleware.cs │ │ ├── PostEventContext.cs │ │ ├── PostEventHandler.cs │ │ ├── PublishedItemHandler.cs │ │ ├── Sequencer.cs │ │ ├── ServerEvent.cs │ │ ├── ServerEventsController.cs │ │ ├── StatusCodeException.cs │ │ └── StatusForbiddenException.cs │ ├── NewVersionChecklist.txt │ ├── Reactive/ │ │ ├── BindableBase.cs │ │ ├── BindingExtensions.cs │ │ ├── BindingOptions.cs │ │ ├── BindingSubscription.cs │ │ ├── CollectionUpdater.cs │ │ └── ObsoleteElement.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Tools/ │ │ ├── ApplicationBuilderLaraExtensions.cs │ │ ├── AssembliesReader.cs │ │ ├── AsyncEvent.cs │ │ ├── ClassEditor.cs │ │ ├── DocumentLocal.cs │ │ ├── LaraBuilder.cs │ │ ├── LaraJson.cs │ │ ├── LaraOptions.cs │ │ ├── LaraTools.cs │ │ ├── NoCurrentSessionException.cs │ │ ├── SemaphoreSlimExtensions.cs │ │ ├── ServerLauncher.cs │ │ └── SessionLocal.cs │ ├── docfx.json │ └── pack.bat ├── LaraUI.sln ├── LaraUI.sln.licenseheader ├── SampleProject/ │ ├── Common/ │ │ ├── CountryList.cs │ │ ├── CountrySelector.cs │ │ ├── SampleAppBootstrap.cs │ │ └── Tools.cs │ ├── Components/ │ │ ├── CheckboxSample.cs │ │ ├── CounterSample.cs │ │ ├── KitchenSinkComponent.cs │ │ ├── LockingSample.cs │ │ ├── LongRunningSample.cs │ │ ├── MultiselectSample.cs │ │ ├── SelectSample.cs │ │ ├── UploadSample.cs │ │ └── WeekdayCombo.cs │ ├── LaraSample.csproj │ ├── Main/ │ │ └── Program.cs │ ├── Pages/ │ │ ├── KitchenSinkPage.cs │ │ ├── ServerEventsPage.cs │ │ └── UploadFilePage.cs │ ├── Properties/ │ │ └── launchSettings.json │ └── SampleProject.csproj └── Tests/ ├── Components/ │ ├── AutocompleteTesting.cs │ └── ComponentTesting.cs ├── DOM/ │ ├── AttributesTesting.cs │ ├── BindingsTesting.cs │ ├── BuilderTesting.cs │ ├── ClassEditorTesting.cs │ ├── DomOperationsTesting.cs │ ├── ElementAttributes.cs │ ├── EventsTesting.cs │ ├── GlobalAttributesTesting.cs │ └── LaraBuilderTesting.cs ├── Delta/ │ ├── AttributeEditTesting.cs │ ├── DeltaTesting.cs │ └── LocatorTesting.cs ├── Main/ │ ├── ButtonCounterPage.cs │ ├── ConnectionTesting.cs │ ├── ConnectionsTesting.cs │ ├── MyPage.cs │ ├── PublishedTesting.cs │ ├── StaleTesting.cs │ └── StaticContentTesting.cs ├── Middleware/ │ ├── DummyContext.cs │ ├── DummyContextTesting.cs │ ├── ErrorPagesTesting.cs │ ├── EventParametersTesting.cs │ ├── MiddlewareTesting.cs │ ├── ServerEventsTesting.cs │ ├── ToolsTesting.cs │ └── WebServicesTesting.cs └── Tests.csproj ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # The following command works for downloading when using Git for Windows: # curl -LOf http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore # # Download this file using PowerShell v3 under Windows with the following comand: # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore # # or wget: # wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore # User-specific files *.suo *.user *.sln.docstates # Build results [Dd]ebug/ [Rr]elease/ x64/ [Bb]in/ [Oo]bj/ # build folder is nowadays used for build scripts and should not be ignored #build/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* *_i.c *_p.c *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.scc # OS generated files # .DS_Store* Icon? # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch *.ncrunch* .*crunch*.local.xml # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.Publish.xml # Windows Azure Build Output csx *.build.csdef # Windows Store app package directory AppPackages/ # Others *.Cache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.[Pp]ublish.xml *.pfx *.publishsettings modulesbin/ tempbin/ # EPiServer Site file (VPP) AppData/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # vim *.txt~ *.swp *.swo # Temp files when opening LibreOffice on ubuntu .~lock.* # svn .svn # CVS - Source Control **/CVS/ # Remainings from resolving conflicts in Source Control *.orig # SQL Server files **/App_Data/*.mdf **/App_Data/*.ldf **/App_Data/*.sdf #LightSwitch generated files GeneratedArtifacts/ _Pvt_Extensions/ ModelManifest.xml # ========================= # Windows detritus # ========================= # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Mac desktop service store files .DS_Store # SASS Compiler cache .sass-cache # Visual Studio 2014 CTP **/*.sln.ide # Visual Studio temp something .vs/ # dotnet stuff project.lock.json # VS 2015+ *.vc.vc.opendb *.vc.db # Rider .idea/ # Visual Studio Code .vscode/ # Output folder used by Webpack or other FE stuff **/node_modules/* **/wwwroot/* # SpecFlow specific *.feature.cs *.feature.xlsx.* *.Specs_*.html ##### # End of core ignore list, below put you custom 'per project' settings (patterns or path) ##### /src/LaraUI/LaraUI.js /src/.axoCover/runs/run_2019-05-14_19-16-46/coverageReport.xml /src/LaraDocumentation/Help /src/LaraClient/build *.map *.js /src/environment.txt /src/Tests/results.xml /codecov /src/LaraClient/dist/assets/images Integrative.Lara.xml ================================================ FILE: .travis.yml ================================================ language: csharp solution: src/LaraUI.sln mono: none dotnet: 5.0.100 before_install: - sudo apt-get update - sudo apt-get install nuget dotnet-sdk-2.1=2.1.300-1 - curl -s -o $HOME/.nvm/nvm.sh https://raw.githubusercontent.com/creationix/nvm/v0.31.0/nvm.sh - source $HOME/.nvm/nvm.sh - nvm install stable - npm -v - node -v - dotnet tool install coveralls.net --tool-path tools install: - nuget restore src/LaraUI.sln script: - cd src - echo "Debug" > environment.txt - cd LaraClient - npm install . - npm run-script build - cd .. - cd .. - dotnet build --configuration Debug src/LaraUI/LaraUI.csproj - dotnet build --configuration Debug --framework net5.0 src/Tests/Tests.csproj - dotnet test --configuration Debug --framework net5.0 src/Tests/Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=results.xml after_script: - REPO_COMMIT_AUTHOR=$(git show -s --pretty=format:"%cn") - REPO_COMMIT_AUTHOR_EMAIL=$(git show -s --pretty=format:"%ce") - REPO_COMMIT_MESSAGE=$(git show -s --pretty=format:"%s") - echo $TRAVIS_COMMIT - echo $TRAVIS_BRANCH - echo $REPO_COMMIT_AUTHOR - echo $REPO_COMMIT_AUTHOR_EMAIL - echo $REPO_COMMIT_MESSAGE - echo $TRAVIS_JOB_ID - ./tools/csmacnz.Coveralls --opencover -i src/Tests/results.xml --repoToken $COVERALLS_TOKEN --commitId $TRAVIS_COMMIT --commitBranch $TRAVIS_BRANCH --commitAuthor "$REPO_COMMIT_AUTHOR" --commitEmail "$REPO_COMMIT_AUTHOR_EMAIL" --commitMessage "$REPO_COMMIT_MESSAGE" --jobId $TRAVIS_JOB_ID --serviceName travis-ci --useRelativePaths ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2019-2020 Integrative Software LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ## Lara Web Engine [![License: Apache 2.0](https://img.shields.io/badge/License-Apache--2.0-blue)](https://github.com/integrativesoft/lara/blob/master/LICENSE) [![NuGet version](http://img.shields.io/nuget/v/Integrative.Lara.svg?nocache=1)](https://www.nuget.org/packages/Integrative.Lara/) [![Download count](https://img.shields.io/nuget/dt/Integrative.Lara.svg)](https://www.nuget.org/packages/Integrative.Lara/) [![Build Status](https://api.travis-ci.com/integrativesoft/lara.svg?branch=master)](https://travis-ci.org/integrativesoft/lara) [![Coverage Status](https://coveralls.io/repos/github/integrativesoft/lara/badge.svg?branch=master&lala=3)](https://coveralls.io/github/integrativesoft/lara?branch=master) ================== **Lara** is a server-side rendering framework for developing **web user interfaces** using C#. >*"It is similar to server-side Blazor, but is much more lightweight and easier to install. For example, while any type of Blazor requires a whole SDK, Lara is just a NuGet package."* [ScientificProgrammer.net](https://scientificprogrammer.net/2019/08/18/pros-and-cons-of-blazor-for-web-development/?pagename=pros-and-cons-of-blazor) ## Sample application ```csharp using Integrative.Lara; using System; using System.Threading.Tasks; namespace SampleApp { public static class Program { public static async Task Main() { // create and start application const int port = 8182; using var app = new Application(); app.PublishPage("/", () => new MyCounterComponent { Value = 5 }); await app.Start(new StartServerOptions { Port = port }); // print address on console var address = $"http://localhost:{port}"; Console.WriteLine($"Listening on {address}/"); // helper function to launch browser (comment out as needed) LaraUI.LaunchBrowser(address); // wait for ASP.NET Core shutdown await app.WaitForShutdown(); } } internal class MyCounterComponent : WebComponent { private int _value; // triggers PropertyChanged event public int Value { get => _value; set => SetProperty(ref _value, value); } public MyCounterComponent() { ShadowRoot.Children = new Node[] { new HtmlDivElement() // on PropertyChanged, assigns InnerText .Bind(this, x => x.InnerText = Value.ToString()), new HtmlButtonElement { InnerText = "Increase" } .Event("click", () => Value++) }; } } } ``` ## Adding Lara to an existing web server application To add Lara to an existing ASP.NET Core server, add to the Startup class or equivalent: ```csharp private readonly Application _laraApp = new Application(); public void Configure(IApplicationBuilder app) { app.UseLara(_laraApp, new LaraOptions { // configuration options }); } ``` ## Creating Desktop applications To create a desktop container for your web app, here's a few options: - [electron.js](https://www.electronjs.org/) combined with [electron-cgi](https://github.com/ruidfigueiredo/electron-cgi#readme) library - [Chromely](https://github.com/chromelyapps/Chromely) - [neutralinojs](https://github.com/neutralinojs/neutralinojs) ## Getting started There's no need to download this repository to use Lara, instead, there's a [NuGet package](https://www.nuget.org/packages/Integrative.Lara/). **Check out the [wiki documentation](https://github.com/integrativesoft/lara/wiki)** ## How does Lara work? Whenever the browser triggers a registered event (e.g. click on a button), it sends to the server a message saying that the button was clicked. The server executes the code associated with the event, manipulating the server's copy of the page, and replies a JSON message with the delta between server and client. ## How to contribute **Please send feedback!** Issues, questions, suggestions, requests for features, and success stories. Please let me know by either opening an issue. Thank you! **If you like Lara, please give it a star - it helps!** ## Credits Thanks to [JetBrains](https://www.jetbrains.com/?from=LaraWebEngine) for the licenses of Rider and DotCover. [![JetBrains](support/jetbrains.svg)](https://www.jetbrains.com/?from=LaraWebEngine) ================================================ FILE: src/Boilerplate/Program.cs ================================================ using Integrative.Lara; using System; using System.Threading.Tasks; namespace SampleApp { public static class Program { public static async Task Main() { // create and start application const int port = 8182; using var app = new Application(); app.PublishPage("/", () => new HttpContextExample()); await app.Start(new StartServerOptions { Port = port }); // print address on console (set project's output type to WinExe to avoid console) var address = $"http://localhost:{port}"; Console.WriteLine($"Listening on {address}/"); // helper function to launch browser (comment out as needed) LaraUI.LaunchBrowser(address); // wait for ASP.NET Core shutdown await app.WaitForShutdown(); } } internal class MyCounterComponent : WebComponent { private int _value; // triggers PropertyChanged event public int Value { get => _value; set => SetProperty(ref _value, value); } public MyCounterComponent() { ShadowRoot.Children = new Node[] { new HtmlDivElement() // on PropertyChanged, assigns InnerText .Bind(this, x => x.InnerText = Value.ToString()), new HtmlButtonElement { InnerText = "Increase" } .Event("click", () => Value++) }; } } } ================================================ FILE: src/Boilerplate/Wiki/ComponentPropertyExample.cs ================================================ using Integrative.Lara; internal class ComponentPropertyExample : WebComponent { public ComponentPropertyExample() { var myLabel = new HtmlSpanElement { InnerText = "Hello!" }; ShadowRoot.Children = new Element[] { new MyLabelComponent { Label = myLabel }, }; } } internal class MyLabelComponent : WebComponent { private Element _label; public Element Label { get => _label; set => SetProperty(ref _label, value); } public MyLabelComponent() { ShadowRoot.Children = new Element[] { new RenderIf(this, () => _label != null, () => _label) }; } } ================================================ FILE: src/Boilerplate/Wiki/ComposingComponent.cs ================================================ using Integrative.Lara; internal class ComposingComponent : WebComponent { public ComposingComponent() // parent component { ShadowRoot.Children = new Element[] { new ItemComponent { Name = "Sara" }, new ItemComponent { Name = "Mike" }, new ItemComponent { Name = "Tom" }, }; } } internal class ItemComponent : WebComponent // child component { private string _name; public string Name { get => _name; set => SetProperty(ref _name, value); } public ItemComponent() { ShadowRoot.Children = new Element[] { new HtmlDivElement { Children = new Element[] { new HtmlTableCellElement() .Bind(this, x => x.InnerText = Name), } } }; } } ================================================ FILE: src/Boilerplate/Wiki/ConditionalRenderComponent.cs ================================================ using Integrative.Lara; internal class ConditionalRenderComponent : WebComponent { private bool _showText; public bool ShowText { get => _showText; set => SetProperty(ref _showText, value); } public ConditionalRenderComponent() { ShadowRoot.Children = new Node[] { new HtmlDivElement { InnerText = "Hello!", } .Bind(this, x => x.Render = ShowText) // Render property here }; } } ================================================ FILE: src/Boilerplate/Wiki/DocumentContextExample.cs ================================================ using Integrative.Lara; internal class DocumentContextExample : WebComponent { const string IconId = "MyIconElement"; public DocumentContextExample() { ShadowRoot.Children = new Element[] { new HtmlDivElement { InnerText = "Check the title and icon of this webpage" } }; } protected override void OnConnect() // our component is placed on Document { base.OnConnect(); var icon = Document.GetElementById(IconId); if (icon != null) return; Document.Head.AppendChild(new HtmlLinkElement { Id = IconId, Rel = "icon", HRef = "https://stackoverflow.com/favicon.ico", }); Document.Head.AppendChild(new HtmlTitleElement { InnerText = "Hello title", }); } } ================================================ FILE: src/Boilerplate/Wiki/HttpContextExample.cs ================================================ using Integrative.Lara; internal class HttpContextExample : WebComponent { private string _message; public string Message { get => _message; set => SetProperty(ref _message, value); } public HttpContextExample() { ShadowRoot.Children = new Element[] { new HtmlDivElement() .Bind(this, x => x.InnerText = Message) }; } protected override void OnConnect() { base.OnConnect(); Message = $"Your IP is {LaraUI.Context.Http.Connection.RemoteIpAddress}"; } } ================================================ FILE: src/Boilerplate/Wiki/LoopComponent.cs ================================================ using Integrative.Lara; using System.Collections.ObjectModel; internal class MyList : WebComponent { private readonly ObservableCollection _names = new ObservableCollection(); public MyList() { ShadowRoot.Children = new Node[] { Fragment.ForEach(_names, (string name) => new HtmlDivElement { InnerText = name }), }; _names.Add("Sarah"); _names.Add("John"); } } ================================================ FILE: src/Boilerplate/Wiki/RedBoxExample.cs ================================================ using Integrative.Lara; internal class RedBoxExample : WebComponent { public RedBoxExample() { ShadowRoot.Children = new Element[] { new RedBoxComponent { Children = new Element[] { new HtmlDivElement { InnerText = "Hello!", } } } }; } } internal class RedBoxComponent : WebComponent { public RedBoxComponent() { ShadowRoot.Children = new Element[] { new HtmlDivElement { Style = "border: solid 3px red", Children = new Element[] { new Slot(), } } }; } } ================================================ FILE: src/Boilerplate/Wiki/SimpleComponent.cs ================================================ using Integrative.Lara; internal class SimpleComponent : WebComponent { private string _message; private string Message { get => _message; set => SetProperty(ref _message, value); } public SimpleComponent() { Message = "My Lara app"; ShadowRoot.Children = new Node[] { new HtmlDivElement { Children = new Node[] { new HtmlSpanElement() .Bind(this, x => x.InnerText = Message) } } }; } } ================================================ FILE: src/Boilerplate/Wiki/UserInputComponent.cs ================================================ using Integrative.Lara; internal class UserInputComponent : WebComponent { int _counter; public int Counter { get => _counter; set => SetProperty(ref _counter, value); } public UserInputComponent() { ShadowRoot.Children = new Element[] { new HtmlDivElement() .Bind(this, x => x.InnerText = Counter.ToString()), new HtmlButtonElement { InnerText = "Increase" } .Event("click", () => Counter++) }; } } ================================================ FILE: src/Boilerplate/Wiki/UserTextComponent.cs ================================================ using Integrative.Lara; internal class UserTextComponent : WebComponent { private string _name; public string Name { get => _name; set => SetProperty(ref _name, value); } public UserTextComponent() { Name = "Taylor"; ShadowRoot.Children = new Element[] { new HtmlDivElement { InnerText = "Please enter your name: " }, new HtmlInputElement() .Bind(this, x => x.Value = Name) // if property changes, update element .BindBack(x => Name = x.Value), // if element changes, update property new HtmlButtonElement { InnerText = "Read" } .Event("click", () => { }), new HtmlDivElement() .Bind(this, x => x.InnerText = $"Your name is {Name}"), }; } } ================================================ FILE: src/Boilerplate/WikiExamples.csproj ================================================ Exe net5.0 WikiExamples SampleApp ================================================ FILE: src/LaraClient/.eslintignore ================================================ node_modules dist .js ================================================ FILE: src/LaraClient/.eslintrc ================================================ { "root": true, "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", "prettier" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "prettier" ], "rules": { "no-console": 1, "prettier/prettier": 2, "no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }] } } ================================================ FILE: src/LaraClient/.prettierrc ================================================ { "semi": false, "trailingComma": "none", "singleQuote": false, "printWidth": 80 } ================================================ FILE: src/LaraClient/LaraClient.njsproj ================================================  14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) LaraClient LaraClient Debug 2.0 6f0bee5a-5c72-4dcb-9173-bd17ba95a76f . False . . v4.0 {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} true false true true Code Code Code Code False True 0 / http://localhost:48022/ False True http://localhost:1337 False CurrentPage True False False False False False ================================================ FILE: src/LaraClient/README.md ================================================ # LaraClient ================================================ FILE: src/LaraClient/dist/lara-client.js.LICENSE.txt ================================================ /*! * Lara Web Engine * Copyright (c) 2019-2020 Integrative Software LLC. * License: Apache-2.0 */ /*! * Sizzle CSS Selector Engine v2.3.5 * https://sizzlejs.com/ * * Copyright JS Foundation and other contributors * Released under the MIT license * https://js.foundation/ * * Date: 2020-03-14 */ /*! * jQuery UI Autocomplete 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ /*! * jQuery UI Keycode 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ /*! * jQuery UI Menu 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ /*! * jQuery UI Position 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/position/ */ /*! * jQuery UI Unique ID 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ /*! * jQuery UI Widget 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license */ ================================================ FILE: src/LaraClient/package.json ================================================ { "name": "lara-client", "version": "0.5.8", "description": "LaraClient", "module": "src/index.ts", "private": true, "author": { "name": "Integrative Software LLC" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --env debug=1", "release": "webpack --env release=1", "clean": "echo \"Webpack clean not needed\" && exit 0", "lint": "eslint . --ext .ts", "prettier-format": "prettier --config .prettierrc 'src/*.ts' --write" }, "devDependencies": { "@types/node": "^16.11.8", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "clean-webpack-plugin": "^4.0.0", "eslint": "^8.2.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "npm-check-updates": "^16.0.5", "prettier": "^2.4.1", "terser-webpack-plugin": "5.2.5", "ts-loader": "^9.2.6", "typescript": "^4.5.2", "webpack": "^5.76.0", "webpack-cli": "^4.9.1" }, "dependencies": { "@types/debounce": "^1.2.1", "@types/jquery": "^3.5.8", "@types/jquery.blockui": "0.0.29", "@types/jqueryui": "^1.12.16", "debounce": "^1.2.1", "webpack-jquery-ui": "^2.0.1" } } ================================================ FILE: src/LaraClient/src/Autocomplete.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ require("webpack-jquery-ui/autocomplete") require("webpack-jquery-ui/css") import { getDocumentId } from "./index" export interface AutocompleteCommand { ElementId: string AutoFocus: boolean MinLength: number Strict: boolean } interface AutocompleteEntry { html?: string label: string subtitle?: string code: string } interface SourceRequest { term: string } interface AutocompleteRequest { Key: string Term: string } type AutocompleteCallback = (_list: AutocompleteEntry[]) => void interface SuccessData { Suggestions: AutocompleteEntry[] } export function autocompleteStart(json: string): void { const step = JSON.parse(json) as AutocompleteCommand const input = document.getElementById(step.ElementId) as HTMLInputElement $(input).autocomplete({ autoFocus: step.AutoFocus, minLength: step.MinLength, source: function ( request: SourceRequest, updater: AutocompleteCallback ): void { $.ajax({ url: "/lara_autocomplete", dataType: "json", data: buildData(input, request.term), success: function (data: SuccessData) { updater(data.Suggestions) }, method: "POST" }) }, select: function (_event, ui: JQueryUI.AutocompleteUIParams): void { const entry = ui.item as AutocompleteEntry setValue(input, entry.code, entry.label) }, change: function (_event, ui: JQueryUI.AutocompleteUIParams): void { if (step.Strict && !ui.item) { setValue(input, "", "") } } }) // eslint-disable-next-line @typescript-eslint/no-explicit-any const instance = $(input).autocomplete("instance") as any instance._renderItem = render } export function autocompleteStop(id: string): void { const input = document.getElementById(id) $(input).autocomplete("destroy") } function setValue(input: HTMLInputElement, value: string, text: string): void { if (!value) { input.removeAttribute("data-lara-value") } else { input.setAttribute("data-lara-value", value) } input.value = text } function render(ul: Element, entry: AutocompleteEntry): JQuery { let html: string if (entry.html) { html = entry.html } else { html = buildLabel(entry) } return $("
  • ").append(html).appendTo(ul) } function buildLabel(entry: AutocompleteEntry): string { const div = $("
    ", { class: "autocompleteEntry" }) const title = $("", { class: "autocompleteTitle" }) title.text(entry.label) div.append(title) if (entry.subtitle) { const subtitle = $('', { class: "autocompleteSubtitle" }) subtitle.text(entry.subtitle) div.append("
    ") div.append(subtitle) } return div.clone().wrap("

    ").parent().html() } function buildData(input: HTMLInputElement, term: string): string { return JSON.stringify(buildRequest(input, term)) } function buildRequest( input: HTMLInputElement, term: string ): AutocompleteRequest { const documentId = getDocumentId() const key = input.id + " " + documentId return { Key: key, Term: term } } ================================================ FILE: src/LaraClient/src/Blocker.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ import { PlugOptions } from "./index" import "./blockUI.js" export function block(plug: PlugOptions): void { if (plug.Block) { const target = resolveTarget(plug) const params = buildParameters(plug) if (target) { $(target).block(params) } else { $.blockUI(params) } } } export function unblock(plug: PlugOptions): void { if (plug.Block) { const target = resolveTarget(plug) if (target) { $(target).unblock() } else { $.unblockUI() } } } function buildParameters(plug: PlugOptions): JQBlockUIOptions { const result: JQBlockUIOptions = {} const shownId = plug.BlockShownId if (shownId) { setElementCSS(result) } else { setDefaultCSS(result) } if (shownId) { result.message = $("#" + shownId) } else if (plug.BlockHTML) { result.message = plug.BlockHTML } else { result.message = null } result.baseZ = 2000 return result } function resolveTarget(plug: PlugOptions): Element { if (plug.BlockElementId) { const el = document.getElementById(plug.BlockElementId) if (el) { return el } } return null } function setElementCSS(options: JQBlockUIOptions): void { options.css = { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", padding: "unset", margin: "unset", border: "unset", width: "unset", "text-align": "unset", color: "unset", "background-color": "unset" } } function setDefaultCSS(options: JQBlockUIOptions): void { options.css = { border: "none", padding: "15px", backgroundColor: "#000", "-webkit-border-radius": "10px", "-moz-border-radius": "10px", "border-radius": "10px", opacity: ".5", color: "#fff", fontSize: "18px", fontFamily: "Verdana,Arial", fontWeight: 200 } } ================================================ FILE: src/LaraClient/src/ContentInterfaces.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ export enum ContentNodeType { // eslint-disable-next-line no-unused-vars Element = 1, // eslint-disable-next-line no-unused-vars Text = 2, // eslint-disable-next-line no-unused-vars Array = 3, // eslint-disable-next-line no-unused-vars Placeholder = 4 } export interface ContentNode { Type: ContentNodeType } export interface ContentTextNode extends ContentNode { Data: string } export interface ContentAttribute { Attribute: string Value: string } export interface ContentElementNode extends ContentNode { TagName: string NS: string Attributes: ContentAttribute[] Children: ContentNode[] } export interface ContentArrayNode extends ContentNode { Nodes: ContentNode[] } export interface ContentPlaceholder extends ContentNode { ElementId: string } ================================================ FILE: src/LaraClient/src/DeltaInterfaces.ts ================================================ /* eslint-disable no-unused-vars */ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ import { ContentNode } from "./ContentInterfaces" import { PlugOptions } from "./index" export enum EventResultType { Success = 0, NoSession = 1, NoElement = 2, OutOfSequence = 3 } export enum DeltaType { Append = 1, Insert = 2, TextModified = 3, Remove = 4, EditAttribute = 5, RemoveAttribute = 6, Focus = 7, SetId = 8, SetValue = 9, SubmitJS = 10, SetChecked = 11, ClearChildren = 12, Replace = 13, ServerEvents = 14, SwapChildren = 15, Subscribe = 16, Unsubscribe = 17, RemoveElementId = 18, Render = 19, UnRender = 20 } export interface BaseDelta { Type: DeltaType } export interface EventResult { ResultType: EventResultType List: BaseDelta[] } export interface NodeAddedDelta extends BaseDelta { ParentId: string Node: ContentNode } export interface NodeInsertedDelta extends BaseDelta { ParentElementId: string Index: number ContentNode: ContentNode } export interface TextModifiedDelta extends BaseDelta { ParentElementId: string ChildNodeIndex: number Text: string } export interface NodeRemovedDelta extends BaseDelta { ParentId: string ChildIndex: number } export interface AttributeEditedDelta extends BaseDelta { ElementId: string Attribute: string Value: string } export interface AttributeRemovedDelta extends BaseDelta { ElementId: string Attribute: string } export interface NodeLocator { StartingId: string ChildIndex?: number } export interface FocusDelta extends BaseDelta { ElementId: string } export interface SetIdDelta extends BaseDelta { OldId: string NewId: string } export interface SetValueDelta extends BaseDelta { ElementId: string Value: string } export interface SubmitJsDelta extends BaseDelta { Code: string Payload?: string } export interface SetCheckedDelta extends BaseDelta { ElementId: string Checked: boolean } export interface ClearChildrenDelta extends BaseDelta { ElementId: string } export interface ReplaceDelta extends BaseDelta { Location: string } export interface SwapChildrenDelta extends BaseDelta { ParentId: string Index1: number Index2: number } export interface SubscribeDelta extends BaseDelta { ElementId: string Settings: PlugOptions DebounceInterval?: number EvalFilter?: string } export interface UnsubscribeDelta extends BaseDelta { ElementId: string EventName: string } export interface RemoveElement extends BaseDelta { ElementId: string } export interface RenderDelta extends BaseDelta { Locator: NodeLocator Node: ContentNode } export interface UnRenderDelta extends BaseDelta { Locator: NodeLocator } ================================================ FILE: src/LaraClient/src/Initializer.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ export function clean(node: Node): void { for (let n = 0; n < node.childNodes.length; n++) { const child = node.childNodes[n] if ( child.nodeType === 8 || (child.nodeType === 3 && !/\S/.test(child.nodeValue)) ) { node.removeChild(child) n-- } else if (child.nodeType === 1) { clean(child) } } } ================================================ FILE: src/LaraClient/src/InputCollector.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ import { PlugOptions } from "./index" export class ElementEventValue { ElementId: string Value: string Checked: boolean } export class ClientEventMessage { Values: ElementEventValue[] ExtraData: string isEmpty(): boolean { return this.Values.length == 0 && !this.ExtraData } } export function collectValues(plug: PlugOptions): FormData { const data = new FormData() const message = collectMessage(plug) const fileCount = collectFiles(plug, data) if (message.isEmpty() && fileCount == 0) { return undefined } else { data.append("_message", JSON.stringify(message)) return data } } export function collectMessage(plug: PlugOptions): ClientEventMessage { const message = new ClientEventMessage() message.Values = [] message.ExtraData = plug.ExtraData collectType("input", message, collectInput) collectType("textarea", message, collectSimpleValue) collectType("button", message, collectSimpleValue) collectType("select", message, collectSimpleValue) collectType("option", message, collectOption) return message } function collectType( tagName: string, message: ClientEventMessage, processor: (_el: Element, _m: ElementEventValue) => void ) { const list = document.getElementsByTagName(tagName) for (let index = 0; index < list.length; index++) { const el = list[index] if (el.id) { const entry = new ElementEventValue() entry.ElementId = el.id processor(el, entry) message.Values.push(entry) } } } function collectInput(el: Element, entry: ElementEventValue): void { const input = el as HTMLInputElement entry.Value = getValue(input) entry.Checked = input.checked } function collectSimpleValue(el: Element, entry: ElementEventValue): void { entry.Value = getValue(el) } function collectOption(el: Element, entry: ElementEventValue): void { const option = el as HTMLOptionElement entry.Checked = option.selected } function getValue(el: Element): string { if (el.hasAttribute("data-lara-value")) { return el.getAttribute("data-lara-value") } else if ("value" in el) { // @ts-ignore return el["value"] } else { return "" } } function collectFiles(plug: PlugOptions, data: FormData): number { if (!plug.UploadFiles) { return 0 } let count = 0 const list = document.getElementsByTagName("input") for (let index = 0; index < list.length; index++) { const input = list[index] as HTMLInputElement count += collectFilesInput(input, data) } return count } function collectFilesInput(input: HTMLInputElement, data: FormData): number { if (!input.id || input.type != "file") { return 0 } const files = input.files const key = "file/" + input.id for (let index = 0; index < files.length; index++) { const file = files[index] data.append(key, file, file.name) } return files.length } ================================================ FILE: src/LaraClient/src/RegisteredEvents.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ import { SubscribeDelta, UnsubscribeDelta } from "./DeltaInterfaces" import { plug, plugEvent, getTargetId } from "./index" import { debounce } from "debounce" const registered = new Map() function addElementEvent( element: EventTarget, eventName: string, handler: EventListener ): void { const id = getTargetId(element) const key = getKey(id, eventName) registered.set(key, handler) element.addEventListener(eventName, handler) } function removeElementEvent(element: EventTarget, eventName: string): void { const id = getTargetId(element) const key = getKey(id, eventName) const handler = registered.get(key) registered.delete(key) element.removeEventListener(eventName, handler) } function getKey(id: string, eventName: string) { return id + " " + eventName } export function subscribe(step: SubscribeDelta): void { const element = getEventTarget(step.ElementId) let handler = buildHandler(element, step) if (step.EvalFilter) { handler = addEvalFilter(handler, step.EvalFilter) } addElementEvent(element, step.Settings.EventName, handler) } function getEventTarget(id: string): EventTarget { if (id) { return document.getElementById(id) } else { return document } } function buildHandler( element: EventTarget, step: SubscribeDelta ): EventListener { if (step.DebounceInterval) { return buildDebouncedHandler(element, step) } else { return buildRegularHandler(element, step) } } function buildDebouncedHandler( element: EventTarget, step: SubscribeDelta ): EventListener { // eslint-disable-next-line @typescript-eslint/no-unused-vars const handler = function (_ev: Event): void { plug(element, step.Settings) } return debounce(handler, step.DebounceInterval) } function buildRegularHandler( element: EventTarget, step: SubscribeDelta ): EventListener { return function (ev: Event): void { plugEvent(element, ev, step.Settings) } } function addEvalFilter(handler: EventListener, filter: string): EventListener { return function (event: Event): void { let run = false try { const result = eval(filter) if (result) { run = true } // eslint-disable-next-line no-empty } catch {} if (run) { handler(event) } } } export function unsubscribe(step: UnsubscribeDelta): void { const element = getEventTarget(step.ElementId) removeElementEvent(element, step.EventName) } ================================================ FILE: src/LaraClient/src/Sequencer.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ type resolver = (_value?: boolean | PromiseLike) => void export class Sequencer { private next: number private pending: Map constructor() { this.next = 1 this.pending = new Map() } async waitForTurn(turn: number): Promise { if (turn == 0) { return true } else if (turn == this.next) { this.next++ this.flushPending() return true } else if (turn > this.next) { let resolver: resolver // eslint-disable-next-line @typescript-eslint/no-unused-vars const task = new Promise((resolve, _reject) => { resolver = resolve }) this.pending.set(turn, resolver) return task } else { return false } } private flushPending(): void { while (this.pending.has(this.next)) { const resolver = this.pending.get(this.next) this.pending.delete(this.next) resolver() this.next++ } } } ================================================ FILE: src/LaraClient/src/SocketEvents.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ import { PlugOptions } from "./index" import { ClientEventMessage } from "./InputCollector" export class EventParameters { DocumentId: string ElementId: string EventName: string EventNumber: number Message: ClientEventMessage } export class SocketEventParameters extends EventParameters { SocketFiles: FormFileCollection } class FormFileCollection { InnerList: FormFile[] constructor() { this.InnerList = [] } } class FormFile { ContentType: string ContentDisposition: string Name: string FileName: string Content: string Length: number } export async function loadFiles( plug: PlugOptions ): Promise { const result = new FormFileCollection() if (!plug.UploadFiles) { return result } const list = document.getElementsByTagName("input") for (let index = 0; index < list.length; index++) { const input = list[index] as HTMLInputElement await collectFilesInput(input, result) } return result } async function collectFilesInput( input: HTMLInputElement, data: FormFileCollection ): Promise { if (!input.id || input.type != "file") { return } const files = input.files const key = "file/" + input.id for (let index = 0; index < files.length; index++) { const file = files[index] const copy = await copyFile(file, key) copy.Name = key data.InnerList.push(copy) } } async function copyFile(file: File, name: string): Promise { const bytes = await readFile(file) const copy = new FormFile() copy.ContentType = file.type copy.Content = bufferToBase64(bytes) copy.Name = name copy.FileName = file.name copy.Length = bytes.byteLength return copy } function bufferToBase64(buffer: ArrayBuffer): string { let binary = "" const bytes = new Uint8Array(buffer) const len = bytes.byteLength for (let index = 0; index < len; index++) { binary += String.fromCharCode(bytes[index]) } return window.btoa(binary) } async function readFile(file: File): Promise { const reader = new FileReader() reader.readAsArrayBuffer(file) return new Promise((resolve, reject) => { reader.onerror = () => { reject("Error reading input file.") } reader.onload = function () { resolve(reader.result as ArrayBuffer) } }) } ================================================ FILE: src/LaraClient/src/Worker.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ import { ContentArrayNode, ContentElementNode, ContentNode, ContentNodeType, ContentPlaceholder, ContentTextNode } from "./ContentInterfaces" import * as Delta from "./DeltaInterfaces" import { listenServerEvents } from "./index" import { subscribe, unsubscribe } from "./RegisteredEvents" export function processResult(steps: Delta.BaseDelta[]): void { for (const step of steps) { if (!processStepCatch(step)) { return } } } function processStepCatch(step: Delta.BaseDelta): boolean { try { processStep(step) return true } catch (Error) { // eslint-disable-next-line no-console console.log("Error processing step:") // eslint-disable-next-line no-console console.log(Error) // eslint-disable-next-line no-console console.log(step) return false } } function processStep(step: Delta.BaseDelta): void { switch (step.Type) { case Delta.DeltaType.Append: append(step as Delta.NodeAddedDelta) break case Delta.DeltaType.Insert: insert(step as Delta.NodeInsertedDelta) break case Delta.DeltaType.TextModified: textModified(step as Delta.TextModifiedDelta) break case Delta.DeltaType.Remove: remove(step as Delta.NodeRemovedDelta) break case Delta.DeltaType.EditAttribute: editAttribute(step as Delta.AttributeEditedDelta) break case Delta.DeltaType.RemoveAttribute: removeAttribute(step as Delta.AttributeRemovedDelta) break case Delta.DeltaType.Focus: focus(step as Delta.FocusDelta) break case Delta.DeltaType.SetId: setId(step as Delta.SetIdDelta) break case Delta.DeltaType.SetValue: setValue(step as Delta.SetValueDelta) break case Delta.DeltaType.SubmitJS: submitJS(step as Delta.SubmitJsDelta) break case Delta.DeltaType.SetChecked: setChecked(step as Delta.SetCheckedDelta) break case Delta.DeltaType.ClearChildren: clearChildren(step as Delta.ClearChildrenDelta) break case Delta.DeltaType.Replace: replaceLocation(step as Delta.ReplaceDelta) break case Delta.DeltaType.ServerEvents: listenServerEvents() break case Delta.DeltaType.SwapChildren: swapChildren(step as Delta.SwapChildrenDelta) break case Delta.DeltaType.Subscribe: subscribe(step as Delta.SubscribeDelta) break case Delta.DeltaType.Unsubscribe: unsubscribe(step as Delta.UnsubscribeDelta) break case Delta.DeltaType.RemoveElementId: removeElementId(step as Delta.RemoveElement) break case Delta.DeltaType.Render: render(step as Delta.RenderDelta) break case Delta.DeltaType.UnRender: unRender(step as Delta.UnRenderDelta) break default: // eslint-disable-next-line no-console console.log( "Error processing event response. Unknown step type: " + step.Type ) } } function append(delta: Delta.NodeAddedDelta): void { const el = document.getElementById(delta.ParentId) const children = createNodes(delta.Node) appendChildren(el, children) } function appendChildren(el: Element, children: Node[]): void { for (const child of children) { el.appendChild(child) } } function insert(delta: Delta.NodeInsertedDelta): void { const el = document.getElementById(delta.ParentElementId) const children = createNodes(delta.ContentNode) if (delta.Index < el.childNodes.length) { const before = el.childNodes[delta.Index] insertBeforeChildren(el, before, children) } else { appendChildren(el, children) } } function render(delta: Delta.RenderDelta): void { const stub = locateNode(delta.Locator) const elements = createNodes(delta.Node) if (elements.length == 0) { stub.remove() return } let last = elements.pop() const parent = stub.parentElement parent.replaceChild(last, stub) while (elements.length) { const pop = elements.pop() parent.insertBefore(pop, last) last = pop } } function insertBeforeChildren( el: Element, before: ChildNode, children: Node[] ): void { for (const child of children) { el.insertBefore(child, before) before = child.nextSibling } } function createNodes(node: ContentNode): Node[] { const list: Node[] = [] pushNodes(node, list) return list } function pushNodes(node: ContentNode, list: Node[]): void { if (node.Type == ContentNodeType.Text) { list.push(createTextNode(node as ContentTextNode)) } else if (node.Type == ContentNodeType.Element) { list.push(createElementNode(node as ContentElementNode)) } else if (node.Type == ContentNodeType.Array) { pushArrayNodes(node as ContentArrayNode, list) } else if (node.Type == ContentNodeType.Placeholder) { list.push(createPlaceholder(node as ContentPlaceholder)) } else { // eslint-disable-next-line no-console console.log( "Error processing event response. Unknown content type: " + node.Type ) document.createTextNode("") } } function pushArrayNodes(node: ContentArrayNode, list: Node[]): void { for (let index = 0; index < node.Nodes.length; index++) { const item = node.Nodes[index] pushNodes(item, list) } } function createTextNode(node: ContentTextNode): Node { const div = document.createElement("div") div.innerHTML = node.Data return document.createTextNode(div.innerText) } function createElementNode(node: ContentElementNode): Element { const child = createRootNode(node) for (const attribute of node.Attributes) { setAttribute(child, attribute.Attribute, attribute.Value) } for (const branch of node.Children) { const nodes = createNodes(branch) for (const node of nodes) { child.appendChild(node) } } return child } function createPlaceholder(node: ContentPlaceholder): Element { const stub = document.createElement("script") stub.id = node.ElementId stub.type = "placeholder/lara" return stub } function setAttribute(child: Element, attribute: string, value: string): void { if (!value) { value = "" } if ( attribute == "value" && (child instanceof HTMLInputElement || child instanceof HTMLSelectElement || child instanceof HTMLTextAreaElement) ) { child.value = value return } if (attribute == "checked" && child instanceof HTMLInputElement) { child.checked = true return } child.setAttribute(attribute, value) } function createRootNode(node: ContentElementNode): Element { if (node.NS) { return document.createElementNS(node.NS, node.TagName) } else { return document.createElement(node.TagName) } } function textModified(delta: Delta.TextModifiedDelta): void { const el = document.getElementById(delta.ParentElementId) const child = el.childNodes[delta.ChildNodeIndex] child.textContent = delta.Text } function remove(delta: Delta.NodeRemovedDelta): void { const parent = document.getElementById(delta.ParentId) const child = parent.childNodes[delta.ChildIndex] child.remove() } function editAttribute(delta: Delta.AttributeEditedDelta): void { const el = document.getElementById(delta.ElementId) if (el.tagName == "OPTION" && delta.Attribute == "selected") { const option = el as HTMLOptionElement option.selected = true } else { el.setAttribute(delta.Attribute, delta.Value) } } function removeAttribute(delta: Delta.AttributeRemovedDelta): void { const el = document.getElementById(delta.ElementId) if (el.tagName == "OPTION" && delta.Attribute == "selected") { const option = el as HTMLOptionElement option.selected = false } else { el.removeAttribute(delta.Attribute) } } function focus(delta: Delta.FocusDelta): void { const el = document.getElementById(delta.ElementId) el.focus() } function setId(delta: Delta.SetIdDelta): void { const el = document.getElementById(delta.OldId) el.id = delta.NewId } function setValue(delta: Delta.SetValueDelta): void { const input = document.getElementById(delta.ElementId) as HTMLInputElement input.value = delta.Value } function submitJS(context: Delta.SubmitJsDelta): void { try { eval(context.Code) } catch (e) { // eslint-disable-next-line no-console console.log((e).message) } } function setChecked(delta: Delta.SetCheckedDelta): void { const input = document.getElementById(delta.ElementId) as HTMLInputElement input.checked = delta.Checked } function clearChildren(delta: Delta.ClearChildrenDelta): void { const parent = document.getElementById(delta.ElementId) while (parent.lastChild) { parent.removeChild(parent.lastChild) } } function replaceLocation(delta: Delta.ReplaceDelta): void { location.replace(delta.Location) } function swapChildren(step: Delta.SwapChildrenDelta): void { const el = document.getElementById(step.ParentId) const node1 = el.childNodes[step.Index1] const node2 = el.childNodes[step.Index2] swapDom(node1, node2) } function swapDom(obj1: Node, obj2: Node): void { const temp = document.createElement("div") obj1.parentNode.insertBefore(temp, obj1) obj2.parentNode.insertBefore(obj1, obj2) temp.parentNode.insertBefore(obj2, temp) temp.parentNode.removeChild(temp) } function removeElementId(delta: Delta.RemoveElement): void { const element = document.getElementById(delta.ElementId) element.remove() } function unRender(delta: Delta.UnRenderDelta): void { const node = locateNode(delta.Locator) const stub = document.createElement("script") if (node instanceof Element) { stub.id = node.id } stub.type = "placeholder/lara" node.parentElement.replaceChild(stub, node) } function locateNode(locator: Delta.NodeLocator): ChildNode { const element = document.getElementById(locator.StartingId) const index = locator.ChildIndex if (index == 0 || index > 0) { return element.childNodes[index] } return element } ================================================ FILE: src/LaraClient/src/custom.d.ts ================================================ declare module "*.svg" { // eslint-disable-next-line @typescript-eslint/no-explicit-any const content: any export default content } ================================================ FILE: src/LaraClient/src/index.ts ================================================ /* Copyright (c) 2019-2020 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ /* Lara is a server-side DOM rendering library for C#. This file is the client runtime for Lara. https://laraui.com */ //#region Framework import { autocompleteStart, autocompleteStop } from "./Autocomplete" import { block, unblock } from "./Blocker" import { EventResult, EventResultType } from "./DeltaInterfaces" import { clean } from "./Initializer" import { collectValues, collectMessage } from "./InputCollector" import { Sequencer } from "./Sequencer" import { processResult } from "./Worker" import { SocketEventParameters, loadFiles } from "./SocketEvents" let documentId: string let lastEventNumber: number let sequencer: Sequencer export function initialize(id: string, keepAliveInterval: number): void { sequencer = new Sequencer() documentId = id lastEventNumber = 0 window.addEventListener("unload", terminate, false) clean(document) const json = document.head.getAttribute("data-lara-initialdelta") if (json) { const result = JSON.parse(json) processEventResult(result) } if (keepAliveInterval) { window.setInterval(sendKeepAlive, keepAliveInterval) } } export function getDocumentId(): string { return documentId } function terminate(): void { const url = "/_discard?doc=" + documentId navigator.sendBeacon(url) } function sendKeepAlive() { const id = getDocumentId() const url = "/_keepAlive?doc=" + id navigator.sendBeacon(url) } export enum PropagationType { // eslint-disable-next-line no-unused-vars Default = 0, // eslint-disable-next-line no-unused-vars StopPropagation = 1, // eslint-disable-next-line no-unused-vars StopImmediatePropagation = 2 } export interface PlugOptions { EventName: string Block?: boolean BlockElementId?: string BlockHTML?: string BlockShownId?: string ExtraData?: string LongRunning?: boolean IgnoreSequence?: boolean Propagation?: PropagationType PreventDefault?: boolean UploadFiles?: boolean } export function plugEvent( el: EventTarget, ev: Event, options: PlugOptions ): void { stopPropagation(ev, options) plug(el, options) } function stopPropagation(ev: Event, options: PlugOptions): void { if (options.PreventDefault) { ev.preventDefault() } if (options.Propagation == PropagationType.StopImmediatePropagation) { ev.stopImmediatePropagation() } else if (options.Propagation == PropagationType.StopPropagation) { ev.stopPropagation() } } export function plug(el: EventTarget, options: PlugOptions): void { if (options.LongRunning) { plugWebSocket(el, options) } else { plugAjax(el, options) } } function plugWebSocket(el: EventTarget, plug: PlugOptions): void { block(plug) const promise = buildSocketParameters(el, plug) promise.then( (params) => { plugWebSocketStart(plug, params) }, (reason) => { // eslint-disable-next-line no-console console.log(reason) location.reload() } ) } function plugWebSocketStart( plug: PlugOptions, params: SocketEventParameters ): void { const url = getSocketUrl("/_event") const socket = new WebSocket(url) // eslint-disable-next-line @typescript-eslint/no-unused-vars socket.onopen = function (_event) { socket.onmessage = async function (e1) { await onSocketMessage(e1.data, params.EventNumber) } // eslint-disable-next-line @typescript-eslint/no-unused-vars socket.onclose = function (_e2) { unblock(plug) } const json = JSON.stringify(params) socket.send(json) } // eslint-disable-next-line @typescript-eslint/no-unused-vars socket.onerror = function (_event) { // eslint-disable-next-line no-console console.log("Error on websocket communication. Reloading.") location.reload() } } function getSocketUrl(name: string): string { let url: string if (location.protocol == "https:") { url = "wss://" } else { url = "ws://" } return url + window.location.host + name } async function buildSocketParameters( el: EventTarget, plug: PlugOptions ): Promise { const params = createSocketParameters(el, plug) params.SocketFiles = await loadFiles(plug) return params } function createSocketParameters( el: EventTarget, plug: PlugOptions ): SocketEventParameters { const params = new SocketEventParameters() if (plug.IgnoreSequence) { params.EventNumber = 0 } else { params.EventNumber = getEventNumber() } params.DocumentId = documentId params.ElementId = getTargetId(el) params.EventName = plug.EventName params.Message = collectMessage(plug) return params } function getEventNumber(): number { lastEventNumber++ return lastEventNumber } async function onSocketMessage( json: string, eventNumber: number ): Promise { await sequencer.waitForTurn(eventNumber) const result = JSON.parse(json) as EventResult processEventResult(result) } export function getTargetId(target: EventTarget): string { if (target instanceof Element) { return target.id } else { return "" } } function plugAjax(el: EventTarget, plug: PlugOptions): void { block(plug) const eventNumber = getEventNumber() const url = getEventUrl(el, plug.EventName, eventNumber) const ajax = new XMLHttpRequest() ajax.onreadystatechange = async function () { if (this.readyState == 4) { await processAjax(this, eventNumber) unblock(plug) } } const data = collectValues(plug) ajax.open("POST", url, true) if (data) { ajax.send(data) } else { ajax.send() } } export interface MessageOptions { key: string data?: string block?: boolean blockElementId?: string blockHtml?: string blockShowElementId?: string longRunning?: boolean } export function sendMessage(options: MessageOptions): void { const params: PlugOptions = { EventName: "_" + options.key, Block: options.block, BlockElementId: options.blockHtml, BlockHTML: options.blockHtml, BlockShownId: options.blockShowElementId, ExtraData: options.data, LongRunning: options.longRunning } plug(document.head, params) } async function processAjax( ajax: XMLHttpRequest, eventNumber: number ): Promise { if (ajax.status == 200) { await processAjaxResult(ajax, eventNumber) } else { processAjaxError(ajax) } } function getEventUrl( el: EventTarget, eventName: string, eventNumber: number ): string { return ( "/_event?doc=" + documentId + "&el=" + getTargetId(el) + "&ev=" + eventName + "&seq=" + eventNumber.toString() ) } async function processAjaxResult( ajax: XMLHttpRequest, eventNumber: number ): Promise { await sequencer.waitForTurn(eventNumber) const result = JSON.parse(ajax.responseText) as EventResult processEventResult(result) } function processEventResult(result: EventResult): void { if (result.ResultType == EventResultType.Success) { if (result.List) { processResult(result.List) } } else if (result.ResultType == EventResultType.NoSession) { location.reload() } } function processAjaxError(ajax: XMLHttpRequest): void { if (ajax.responseText) { document.write(ajax.responseText) } else { // eslint-disable-next-line no-console console.log( "Internal Server Error on AJAX call. Detailed exception information on the client is turned off." ) } } export function listenServerEvents(): void { plugWebSocket(document.head, { EventName: "_server_event", Block: false, ExtraData: "", LongRunning: true, IgnoreSequence: true }) } //#endregion //#region Built-in web components export function autocompleteApply(payload: string): void { autocompleteStart(payload) } export function autocompleteDestroy(id: string): void { autocompleteStop(id) } //#endregion ================================================ FILE: src/LaraClient/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "lib": [ "es6", "dom" ], "sourceMap": true, "allowJs": true } } ================================================ FILE: src/LaraClient.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29519.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "LaraClient", "LaraClient\LaraClient.njsproj", "{6F0BEE5A-5C72-4DCB-9173-BD17BA95A76F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F0BEE5A-5C72-4DCB-9173-BD17BA95A76F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F0BEE5A-5C72-4DCB-9173-BD17BA95A76F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F0BEE5A-5C72-4DCB-9173-BD17BA95A76F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F0BEE5A-5C72-4DCB-9173-BD17BA95A76F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3770CCA5-4282-46C3-9C5D-65FC0534FF93} EndGlobalSection EndGlobal ================================================ FILE: src/LaraDocumentation/Content/Welcome.aml ================================================ Use the menu on the left to navigate. ================================================ FILE: src/LaraDocumentation/ContentLayout.content ================================================  ================================================ FILE: src/LaraDocumentation/LaraDocumentation.shfbproj ================================================  Debug AnyCPU 2.0 0019560b-ed95-4921-9050-8ad37bf9d7ef 2017.9.26.0 LaraDocumentation LaraDocumentation LaraDocumentation .NET Framework 4.5 .\Help\ LaraDocumentation en-US Website C#, F# VS2013 True True False False OnlyWarningsAndErrors 100 Lara Documentation 1.0.0.0 False False 2 False Blank ProtectedInternalAsProtected, NonBrowsable Copyright %28c%29 2020 Integrative Software LLC Lara's namespace Integrative.Lara namespace LaraUI {1cf006b1-8b28-45bc-b9c8-1e548acd115d} True OnBuildSuccess ================================================ FILE: src/LaraUI/.gitignore ================================================ ############### # folder # ############### /**/DROP/ /**/TEMP/ /**/packages/ /**/bin/ /**/obj/ _site ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { ///

    /// Autocomplete web component /// public class AutocompleteElement : WebComponent { /// /// Autocomplete's custom HTML tag /// public const string CustomTag = "lara-autocomplete"; /// /// Returns the inner input element /// public HtmlInputElement InnerInput { get; } = new HtmlInputElement(); /// /// Constructor /// public AutocompleteElement() : base(CustomTag) { InnerInput.Autocomplete = "off"; ShadowRoot.AppendChild(InnerInput); } /// /// Input element's class /// public override string? Class { get => InnerInput.Class; set => InnerInput.Class = value; } private bool _pending, _applied; private AutocompleteOptions? _options; internal AutocompleteOptions? GetOptions() => _options; /// /// Enables the autocomplete functionality /// /// Autocomplete options public void Start(AutocompleteOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); if (Document == null) { _pending = true; } else { DestroyAutocomplete(); SubmitAutocomplete(Document, options); } } /// /// Stops the autocomplete functionality /// public void Stop() { DestroyAutocomplete(); } /// /// Establishes autocomplete if pending /// protected override void OnConnect() { if (!_pending) return; _pending = false; DestroyAutocomplete(); if (Document != null && _options != null) { SubmitAutocomplete(Document, _options); } } /// /// Disposes autocomplete references on browser /// protected override void OnDisconnect() { DestroyAutocomplete(); } internal string AutocompleteId { get; private set; } = string.Empty; private void SubmitAutocomplete(Document document, AutocompleteOptions options) { AutocompleteId = GetAutocompleteKey(document); AutocompleteService.Register(AutocompleteId, this); _applied = true; _pending = false; var payload = new AutocompletePayload { AutoFocus = options.AutoFocus, ElementId = InnerInput.Id, MinLength = options.MinLength, Strict = options.Strict }; var json = LaraUI.JSON.Stringify(payload); var code = $"LaraUI.autocompleteApply(context.Payload);"; LaraUI.Page.JSBridge.Submit(code, json); } private string GetAutocompleteKey(Document document) { return InnerInput.Id + " " + document.VirtualIdString; } private void DestroyAutocomplete() { if (!_applied) return; _applied = false; AutocompleteService.Unregister(AutocompleteId); var code = $"LaraUI.autocompleteDestroy('{InnerInput.Id}');"; LaraUI.Page.JSBridge.Submit(code); } /// /// Value property /// public string? Value { get => InnerInput.Value; set => InnerInput.Value = value; } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteEntry.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { /// /// Default implementation for IAutocompleteEntry /// [DataContract] public class AutocompleteEntry { /// /// Optional custom HTML for the autocomplete row shown /// /// [DataMember(Name = "html")] public string? Html { get; set; } /// /// Text to fill the input control when selected /// [DataMember(Name = "label")] public string? Label { get; set; } /// /// Value property associated with the entry /// [DataMember(Name = "code")] public string? Code { get; set; } /// /// Subtitle to show when using default HTML /// [DataMember(Name = "subtitle")] public string? Subtitle { get; set; } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// Autocomplete options /// public class AutocompleteOptions { /// /// Minimum number of characters required to trigger autocomplete suggestions /// public int MinLength { get; set; } /// /// Automatically focus on selection list /// public bool AutoFocus { get; set; } = true; /// /// When true, only the suggested values can be selected /// public bool Strict { get; set; } /// /// Autocomplete suggestions provider /// public IAutocompleteProvider? Provider { get; set; } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompletePayload.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class AutocompletePayload { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public bool AutoFocus { get; set; } [DataMember] public int MinLength { get; set; } [DataMember] public bool Strict { get; set; } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteRegistry.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Integrative.Lara { internal class AutocompleteRegistry { private readonly SessionLocal> _map; private readonly object _mapLock = new object(); public AutocompleteRegistry() { _map = new SessionLocal>(); } public bool TryGet(string key, [NotNullWhen(true)] out AutocompleteElement? element) { element = default; lock (_mapLock) { var map = _map.Value; return map != null && map.TryGetValue(key, out element); } } public void Set(string key, AutocompleteElement element) { lock (_mapLock) { var map = _map.Value; if (map == null) { map = new Dictionary(); _map.Value = map; map.Add(key, element); } else { map.Remove(key); map.Add(key, element); } } } public void Remove(string key) { lock (_mapLock) { _map.Value?.Remove(key); } } public int Count => GetCount(); private int GetCount() { var value = GetValue(); return value?.Count ?? 0; } private Dictionary? GetValue() { lock (_mapLock) { return _map.Value; } } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteResponse.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { /// /// Autocomplete results /// [DataContract] public class AutocompleteResponse { /// /// List of autocomplete entries /// [DataMember] public List? Suggestions { get; set; } } } ================================================ FILE: src/LaraUI/Autocomplete/AutocompleteService.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; using System.Threading.Tasks; namespace Integrative.Lara { [DataContract] internal class AutocompleteRequest { [DataMember] public string Key { get; set; } = string.Empty; [DataMember] public string Term { get; set; } = string.Empty; } internal class AutocompleteService : IWebService { public const string Address = "/lara_autocomplete"; private static readonly AutocompleteRegistry _Map = new AutocompleteRegistry(); public Task Execute() { return Execute(LaraUI.Service.RequestBody); } internal static async Task Execute(string json) { var request = LaraUI.JSON.Parse(json); if (!_Map.TryGet(request.Key, out var element)) { return string.Empty; } var options = element.GetOptions(); if (options?.Provider == null) { return string.Empty; } var response = await options.Provider.GetAutocompleteList(request.Term); return LaraUI.JSON.Stringify(response); } public static void Register(string key, AutocompleteElement element) { _Map.Set(key, element); } public static void Unregister(string key) { _Map.Remove(key); } public static int RegisteredCount => _Map.Count; } } ================================================ FILE: src/LaraUI/Autocomplete/IAutocompleteProvider.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { /// /// Interface for a class that provides autocomplete suggestions /// public interface IAutocompleteProvider { /// /// Method that provides autocomplete suggestions /// /// Search term typed /// Autocomplete response Task GetAutocompleteList(string term); } } ================================================ FILE: src/LaraUI/Components/ComponentRegistry.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; namespace Integrative.Lara { internal sealed class ComponentRegistry { private readonly Dictionary _components; public ComponentRegistry() { _components = new Dictionary(); } public void Register(string name, Type type) { name = name ?? throw new ArgumentNullException(nameof(name)); type = type ?? throw new ArgumentNullException(nameof(type)); if (!IsValidTagName(name)) { throw new ArgumentException(Resources.DashRequired); } if (!type.IsSubclassOf(typeof(WebComponent))) { throw new InvalidOperationException(Resources.MustInherit); } if (_components.TryGetValue(name, out var previous)) { var message = $"Duplicate entries for tag '{name}'. The class '{previous.FullName}' already registers the tag name."; throw new InvalidOperationException(message); } _components.Add(name, type); } public void Unregister(string tagName) { _components.Remove(tagName); } private static bool IsValidTagName(string tagName) { return !string.IsNullOrEmpty(tagName) && !tagName.Contains(" ", StringComparison.InvariantCulture) && tagName.Contains("-", StringComparison.InvariantCulture); } public bool TryGetComponent(string name, out Type type) { return _components.TryGetValue(name, out type); } public void Clear() { _components.Clear(); } } } ================================================ FILE: src/LaraUI/Components/Fragment.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 1/2021 Author: Pablo Carbonell */ using System; using System.Collections.ObjectModel; namespace Integrative.Lara { /// /// Fragment component to group element in a single parent /// public class Fragment : WebComponent { /// /// Constructor /// public Fragment() { ShadowRoot.AppendChild(new Slot()); } /// /// Creates a Fragment with a list of children that follows an observable collection /// /// /// /// /// public static Fragment ForEach(ObservableCollection source, Func factory) { var fragment = new Fragment(); fragment.BindChildren(source, factory); return fragment; } } } ================================================ FILE: src/LaraUI/Components/LaraWebComponent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Classes marked as LaraWebComponent will registered when executing LaraUI.PublishAssemblies() /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class LaraWebComponentAttribute : Attribute { /// /// Custom tag name for the component. Must contain the '-' character. /// public string ComponentTagName { get; } /// /// Constructor with custom tag name /// /// Tag name of the web component public LaraWebComponentAttribute(string customTagName) { ComponentTagName = customTagName; } /// /// Constructor /// public LaraWebComponentAttribute() : this("") { } } } ================================================ FILE: src/LaraUI/Components/RenderIf.cs ================================================ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Conditional render placeholder /// public class RenderIf : WebComponent { private readonly Func _criteria; private readonly Func _factory; private bool _rendered; /// /// Constructor /// /// /// /// public RenderIf(INotifyPropertyChanged source, Func criteria, Func factory) { _criteria = criteria; _factory = factory; source.PropertyChanged += (_, _) => Update(); } private void Update() { var rendered = _criteria(); if (rendered == _rendered) return; _rendered = rendered; if (rendered) { ShadowRoot.AppendChild(_factory()); } else { ShadowRoot.ClearChildren(); } } } } ================================================ FILE: src/LaraUI/Components/Shadow.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Linq; namespace Integrative.Lara { internal sealed class Shadow : Element { private const string ShadowTagName = "__shadow"; public Shadow(WebComponent parent) : base(ShadowTagName) { ParentComponent = parent; } public WebComponent ParentComponent { get; } internal override IEnumerable GetLightSlotted() { return Enumerable.Empty(); } internal override bool IsPrintable => false; } } ================================================ FILE: src/LaraUI/Components/Slot.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Integrative.Lara { /// /// A slot element is a placeholder inside a web component that you can fill with your own element /// public sealed class Slot : Element { /// /// The slot's name /// public string? Name { get => GetAttribute("name"); set => SetAttributeLower("name", value); } /// /// Constructor /// public Slot() : base("slot") { } internal bool MatchesName(string? slotName) { var name = Name; if (string.IsNullOrEmpty(name)) { return string.IsNullOrEmpty(slotName); } return name == slotName; } internal override IEnumerable GetLightSlotted() { return TryFindParentComponent(this, out var component) ? component.GetSlottedElements(Name) : Enumerable.Repeat(this, 1); } private static bool TryFindParentComponent(Node element, [NotNullWhen(true)] out WebComponent? component) { var parent = element.ParentElement; if (parent is null) { component = default; return false; } if (parent is not Shadow shadow) return TryFindParentComponent(parent, out component); component = shadow.ParentComponent; return true; // ReSharper disable once TailRecursiveCall } internal override bool IsPrintable => false; } } ================================================ FILE: src/LaraUI/Components/SlottedCalculator.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { internal static class SlottedCalculator { public static void UpdateSlotted(Node node) { node.IsSlotted = IsParentSlotting(node); if (node is WebComponent component) { var shadow = component.GetShadow(); UpdateSlotted(shadow); } if (node is Element element) { UpdateChildren(element); } } internal static bool IsParentSlotting(Node node) { var parent = node.ParentElement; if (parent == null || parent is Slot) { return false; } if (parent is Shadow shadow) { return shadow.ParentComponent.IsSlotted; } if (parent is WebComponent component) { return node is Element element && component.IsSlotActive(element.GetAttributeLower("slot")); } return parent.IsSlotted; } private static void UpdateChildren(Element element) { foreach (var node in element.Children) { UpdateSlotted(node); } } } } ================================================ FILE: src/LaraUI/Components/WebComponent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; namespace Integrative.Lara { /// /// Base class for web components /// public abstract class WebComponent : Element { /// /// The 'shadow root' is the element that contains the shadow DOM tree /// protected Element ShadowRoot => _shadow; private readonly Shadow _shadow; internal Shadow GetShadow() => _shadow; private HashSet? _observedAttributes; /// /// Constructor /// /// Component's custom tag name protected WebComponent(string tagName) : base(tagName ?? throw new ArgumentNullException(nameof(tagName))) { VerifyTypeThrow(tagName, GetType()); _shadow = new Shadow(this); } /// /// Constructor /// protected WebComponent() { VerifyTypeThrow(TagName, GetType()); _shadow = new Shadow(this); } private void InitializeObservedAttributes() { if (_observedAttributes == null) { _observedAttributes = new HashSet(GetObservedAttributes()); } } private static void VerifyTypeThrow(string tagName, Type componentType) { if (!VerifyType(tagName, componentType, out var error)) { throw new InvalidOperationException(error); } } internal static bool VerifyType(string tagName, Type componentType, out string error) { // register component if not previous;y registered var app = LaraUI.Context.Application; if (!app.TryGetComponent(tagName, out var type)) { app.PublishComponent(new WebComponentOptions { ComponentTagName = tagName, ComponentType = componentType }); error = ""; return true; } // error if already registered for different type if (type != componentType) { error = $"The tag '{tagName}' is registered with the type '{type.FullName}' and not '{componentType.FullName}'."; return false; } // already registered with matching type error = string.Empty; return true; } /// /// Obsolete /// [Obsolete("Not needed anymore, Shadow root is automatically created")] [EditorBrowsable(EditorBrowsableState.Never)] protected void AttachShadow() { } /// /// Override to declare a list of attributes that will trigger the OnAttributeChanged event /// /// protected virtual IEnumerable GetObservedAttributes() { return Enumerable.Empty(); } internal override IEnumerable GetLightSlotted() { foreach (var child in ShadowRoot.Children) { if (child is Element childElement) { foreach (var light in childElement.GetLightSlotted()) { yield return light; } } else { yield return child; } } } internal override IEnumerable GetAllDescendants() { yield return ShadowRoot; foreach (var child in Children) { yield return child; } } /// /// Returns the elements that are slotted with the given slot name /// /// Slot name /// IEnumerable of nodes public IEnumerable GetSlottedElements(string? slotName) { foreach (var node in Children) { if (NodeMatchesSlot(node, slotName)) { yield return node; } } } private static bool NodeMatchesSlot(Node node, string? slotName) { return node is Element element && ElementMatchesSlot(element, slotName); } private static bool ElementMatchesSlot(Element element, string? slotName) { var slot = element.GetAttributeLower("slot"); if (string.IsNullOrEmpty(slotName)) { return string.IsNullOrEmpty(slot); } return slot == slotName; } internal override void AttributeChanged(string attribute, string? value) { BeginUpdate(); base.AttributeChanged(attribute, value); InitializeObservedAttributes(); if (_observedAttributes != null && _observedAttributes.Contains(attribute)) { OnAttributeChanged(attribute); } EndUpdate(); } /// /// Invoked each time an attribute defined in GetObservedAttributes is modified. /// /// protected virtual void OnAttributeChanged(string attribute) { } internal override IEnumerable GetNotifyList() { foreach (var child in base.GetNotifyList()) { yield return child; } yield return ShadowRoot; } internal override bool IsPrintable => false; internal bool IsSlotActive(string? slotName) { return IsSlotActive(ShadowRoot, slotName); } private static bool IsSlotActive(Element parent, string? slotName) { foreach (var child in parent.Children) { if (IsSlotChildActive(child, slotName)) { return true; } } return false; } private static bool IsSlotChildActive(Node child, string? slotName) { return (child is Slot slot && slot.MatchesName(slotName)) || (child is Element element && IsSlotActive(element, slotName)); } /// /// Triggers a custom event /// /// Event's name public void TriggerEvent(string eventName) => NotifyEvent(eventName); } } ================================================ FILE: src/LaraUI/Components/WebComponentOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Options for publishing web components /// public sealed class WebComponentOptions { /// /// Custom tag name for the component. Needs to include the '-' character. /// public string ComponentTagName { get; set; } = string.Empty; /// /// Type of the component. Needs to inherit from WebComponent. Example: 'typeof(MyComponent)' /// public Type? ComponentType { get; set; } internal Type GetComponentType() { return ComponentType ?? throw new MissingMemberException(nameof(WebComponentOptions), nameof(ComponentType)); } } } ================================================ FILE: src/LaraUI/DOM/Attributes.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections; using System.Collections.Generic; namespace Integrative.Lara { internal sealed class Attributes : IEnumerable> { private readonly Element _element; private readonly Dictionary _values; public Attributes(Element element) { _element = element; _values = new Dictionary(); SetAttributeLower("id", element.Id); } public bool HasAttribute(string name) => HasAttributeLower(name.ToLowerInvariant()); public string? GetAttribute(string name) => GetAttributeLower(name.ToLowerInvariant()); internal bool HasAttributeLower(string nameLower) => _values.ContainsKey(nameLower); internal void SetAttributeLower(string nameLower, string? value) { if (nameLower == "slot" && _element.ParentElement != null) { throw new InvalidOperationException(Resources.SlotOnlyParent); } if (_values.TryGetValue(nameLower, out var previous)) { if (previous == value) { return; } _values.Remove(nameLower); } _values.Add(nameLower, value); if (nameLower == "value") { SetValueDelta.Enqueue(_element, value); } else if (nameLower == "checked") { SetCheckedDelta.Enqueue(_element, true); } else if (nameLower == "id") { SetIdDelta.Enqueue(_element, value ?? ""); } else { AttributeEditedDelta.Enqueue(_element, nameLower, value); } _element.AttributeChanged(nameLower, value); if (nameLower == "slot") { _element.UpdateSlotted(); } } internal void RemoveAttributeLower(string nameLower) { if (!_values.ContainsKey(nameLower)) { return; } _values.Remove(nameLower); if (nameLower == "checked") { SetCheckedDelta.Enqueue(_element, false); } else { AttributeRemovedDelta.Enqueue(_element, nameLower); } _element.AttributeChanged(nameLower, null); } internal void SetFlagAttributeLower(string nameLower, bool value) { var current = _values.ContainsKey(nameLower); if (value == current) return; if (value) { SetAttributeLower(nameLower, ""); } else { RemoveAttributeLower(nameLower); } } internal void NotifyValue(string value) { const string valueAttribute = "value"; if (_values.TryGetValue(valueAttribute, out var previous)) { if (previous == value) { return; } _values.Remove(valueAttribute); } _values.Add(valueAttribute, value); _element.AttributeChanged(valueAttribute, value); } internal void NotifyChecked(bool isChecked) { NotifyFlag("checked", isChecked); } internal void NotifySelected(bool selected) { NotifyFlag("selected", selected); } private void NotifyFlag(string nameLower, bool value) { var current = _values.ContainsKey(nameLower); if (current == value) { return; } if (value) { _values.Add(nameLower, null); } else { _values.Remove(nameLower); } _element.AttributeChanged(nameLower, string.Empty); } internal string? GetAttributeLower(string nameLower) { return _values.TryGetValue(nameLower, out var result) ? result : string.Empty; } public IEnumerator> GetEnumerator() { return _values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _values.GetEnumerator(); } } } ================================================ FILE: src/LaraUI/DOM/BlockOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// Defines options for blocking the UI while executing an event /// public class BlockOptions { /// /// Gets or sets the ID of element to block. Leave empty to block the whole page. /// /// /// The ID of the element to block. If left blank, block the entire page. /// public string? BlockedElementId { get; set; } /// /// Gets or sets the ID of the element to show. If set, the element specified will be shown instead of the default block dialog. /// /// /// The ID of the element to show instead of the default block dialog. /// public string? ShowElementId { get; set; } /// /// Gets or sets an HTML message to show while blocking the user interface. If set, the specified HTML will be shown instead of the default block dialog. /// /// /// The HTML message to show instead of the default block dialog. /// public string? ShowHtmlMessage { get; set; } } } ================================================ FILE: src/LaraUI/DOM/ChildrenBindingSubscription.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System.Collections.Specialized; namespace Integrative.Lara { internal class ChildrenBindingSubscription { public NotifyCollectionChangedEventHandler Handler { get; } public INotifyCollectionChanged Source { get; } public ChildrenBindingSubscription( NotifyCollectionChangedEventHandler handler, INotifyCollectionChanged source) { Handler = handler; Source = source; source.CollectionChanged += handler; } public void Unsubscribe() { Source.CollectionChanged -= Handler; } } } ================================================ FILE: src/LaraUI/DOM/Document.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Globalization; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Status options for server-side events /// public enum ServerEventsStatus { /// /// Server-side events have not been enabled /// Disabled, /// /// The server is waiting for the client to listen to server-side events /// Connecting, /// /// Server-side events are enabled /// Enabled } /// /// An HTML5 document. /// public class Document { internal IPage Page { get; } /// /// Global unique identifier for the document /// public Guid VirtualId { get; } private readonly DocumentIdMap _map; private readonly Queue _queue; internal SemaphoreSlim Semaphore { get; } private readonly ServerEventsController _serverEvents; private readonly MessageRegistry _messageRegistry; private readonly Sequencer _sequencer = new(); private Dictionary Events { get; } = new(); /// /// Occurs when the document is unloaded /// public event EventHandler? OnUnload; /// /// Asynchronous unload event /// public AsyncEvent OnUnloadAsync { get; } = new(); internal event EventHandler? UnloadComplete; /// /// Gets or sets the language. See 'lang' property for HTML5 documents. /// /// /// The language. /// public string? Lang { get; set; } /// /// The document's Head element. /// /// /// The head. /// public HtmlHeadElement Head { get; } /// /// The document's Body element. /// /// /// The body. /// public HtmlBodyElement Body { get; } internal DateTime LastUtc { get; private set; } internal Queue GetQueue() => _queue; internal Document(IPage page, double keepAliveInterval) : this(page, Connections.CreateCryptographicallySecureGuid(), keepAliveInterval) { } internal Document(IPage page, Guid virtualId, double keepAliveInterval) { VirtualId = virtualId; Page = page; _map = new DocumentIdMap(); _queue = new Queue(); Semaphore = new SemaphoreSlim(1); Head = new HtmlHeadElement { Document = this, IsSlotted = true }; OnElementAdded(Head); Body = new HtmlBodyElement { Document = this, IsSlotted = true }; UpdateTimestamp(); OnElementAdded(Body); TemplateBuilder.Build(this, keepAliveInterval); _serverEvents = new ServerEventsController(this); _messageRegistry = new MessageRegistry(this); } internal string VirtualIdString => VirtualId.ToString(GlobalConstants.GuidFormat, CultureInfo.InvariantCulture); /// /// Creates an HTML element. /// /// Name of the tag. /// Element created public static Element CreateElement(string tagName) => Element.Create(tagName); /// /// Creates a text node. /// /// The node's data. /// Text node created public static TextNode CreateTextNode(string data) => new TextNode(data); internal void UpdateTimestamp() { LastUtc = DateTime.UtcNow; } internal void ModifyLastUtcForTesting(DateTime value) { LastUtc = value; } internal void OnElementAdded(Element element) => _map.NotifyAdded(element); internal void OnElementRemoved(Element element) => _map.NotifyRemoved(element); internal void NotifyChangeId(Element element, string before, string after) => _map.NotifyChangeId(element, before, after); internal bool QueueingEvents { get; private set; } internal void OpenEventQueue() { if (_queue.Count > 0) { var json = FlushQueue(); Head.SetAttribute("data-lara-initialdelta", json); } QueueingEvents = true; } internal string FlushQueue() { var list = new List(); while (_queue.Count > 0) { var step = _queue.Dequeue(); list.Add(step); } var result = new EventResult(list); return result.ToJSON(); } internal void Enqueue(BaseDelta delta) { _queue.Enqueue(delta); } /// /// Retrieves the element with the given ID. /// /// Element ID /// The element. /// True when the element was found. public bool TryGetElementById(string id, out Element element) => _map.TryGetElementById(id, out element); /// /// Retrieves the element with the given ID. /// /// Element ID /// The element public Element GetElementById(string id) { _map.TryGetElementById(id, out var element); return element; } /// /// Returns true when there are UI changes pending to be flushed to the client /// public bool HasPendingChanges => _queue.Count > 0; [Obsolete("Support old methods")] internal void OnMessage(string key, Func handler) { Head.On("_" + key, handler); } internal void AddMessageListener(string messageId, Func handler) { _messageRegistry.Add(messageId, handler); } internal void RemoveMessageListener(string messageId, Func handler) { _messageRegistry.Remove(messageId, handler); } internal async Task NotifyUnload() { await _serverEvents.NotifyUnload(); var args = new EventArgs(); OnUnload?.Invoke(this, args); await OnUnloadAsync.InvokeAsync(this, args); UnloadComplete?.Invoke(this, new EventArgs()); _sequencer.AbortAll(); } /// /// Returns the current status of server-side events /// public ServerEventsStatus ServerEventsStatus => _serverEvents.ServerEventsStatus; /// /// Starts a server event. Call with 'using' and dispose the class returned. /// /// Disposable token public ServerEvent StartServerEvent() => _serverEvents.StartServerEvent(); internal void ServerEventsOn() { NotifyHasEvent(); _serverEvents.ServerEventsOn(); } internal Task ServerEventsOff() => _serverEvents.ServerEventsOff(); internal bool SocketRemainsOpen(string eventName) => _serverEvents.SocketRemainsOpen(eventName); internal virtual Task> GetSocketCompletion(WebSocket socket) => _serverEvents.GetSocketCompletion(socket); internal Task ServerEventFlush() => _serverEvents.ServerEventFlush(); internal ServerEventsController GetServerEventsController() => _serverEvents; internal Task WaitForTurn(long turn) => _sequencer.WaitForTurn(turn); internal bool CanDiscard { get; private set; } = true; internal void NotifyHasEvent() { CanDiscard = false; } internal Task NotifyEvent(string eventName) { if (Events.TryGetValue(eventName, out var settings) && settings.Handler != null) { return settings.Handler(); } return Task.CompletedTask; } /// /// Registers an event and associates code to execute. /// /// The event's settings. public void On(EventSettings settings) { settings = settings ?? throw new ArgumentNullException(nameof(settings)); settings.Verify(); RemoveEvent(settings.EventName); Events.Add(settings.EventName, settings); SubscribeDelta.Enqueue(this, settings); } /// /// Registers an event and associates code to execute. /// /// Name of the event. /// The handler to execute. public void On(string eventName, Func? handler) { eventName = eventName ?? throw new ArgumentNullException(nameof(eventName)); if (handler == null) { RemoveEvent(eventName); } else { On(new EventSettings { EventName = eventName, Handler = handler }); } } private void RemoveEvent(string eventName) { if (!Events.ContainsKey(eventName)) return; Events.Remove(eventName); Enqueue(new UnsubscribeDelta { ElementId = string.Empty, EventName = eventName }); } } } ================================================ FILE: src/LaraUI/DOM/DocumentIdMap.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Collections.Generic; namespace Integrative.Lara { internal sealed class DocumentIdMap { private readonly Dictionary _map; public DocumentIdMap() { _map = new Dictionary(); } public bool TryGetElementById(string id, out Element element) { return _map.TryGetValue(id, out element); } public void NotifyChangeId(Element element, string before, string after) { if (before == after) return; RemovePrevious(before); AddAfter(element, after); } private void RemovePrevious(string before) { _map.Remove(before); } private void AddAfter(Element element, string after) { if (_map.ContainsKey(after)) { throw DuplicateElementIdException.Create(after); } _map.Add(after, element); } public void NotifyRemoved(Element element) { RemovePrevious(element.Id); } public void NotifyAdded(Element element) { AddAfter(element, element.Id); } } } ================================================ FILE: src/LaraUI/DOM/DocumentWriter.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Globalization; using System.Text; using System.Web; namespace Integrative.Lara { internal sealed class DocumentWriter { public const int MaxLevelDeep = 500; private readonly Document? _document; private readonly StringBuilder _builder; public DocumentWriter(Document document) : this() { _document = document; } public DocumentWriter() { _builder = new StringBuilder(); } public void Print() { if (_document == null) { return; } _builder.AppendLine(""); _builder.AppendLine(""); _builder.Append(""); PrintElement(_document.Head, 1); PrintElement(_document.Body, 1); _builder.AppendLine(""); } public void PrintElement(Element element, int indent) { VerifyNestedLevel(indent); if (!element.Render) { PrintStubElement(element); } else if (IsInlineElement(element)) { Indent(indent); PrintInlineElement(element); _builder.AppendLine(); } else { PrintRegularElement(element, indent); } } internal static void VerifyNestedLevel(int indent) { if (indent > MaxLevelDeep) { throw new InvalidOperationException($"Document exceeded maximum nesting level of {MaxLevelDeep.ToString(CultureInfo.CurrentCulture)}."); } } private static bool IsInlineElement(Element element) { var hasChildren = false; foreach (var child in element.GetLightChildren()) { hasChildren = true; if (child is TextNode) { return true; } } return !hasChildren; } private void PrintInlineElement(Element element) { PrintOpeningTag(element); if (HtmlReference.IsSelfClosingTag(element.TagName)) return; PrintInlineChildNodes(element); PrintClosingTag(element, 0); } private void PrintStubElement(Element element) { _builder.Append(""); } private void Indent(int indent) { for (var index = 0; index < indent; index++) { _builder.Append('\t'); } } private void PrintOpeningTag(Element element) { _builder.Append('<'); _builder.Append(element.TagName); PrintAttributes(element); _builder.Append('>'); } private void PrintAttributes(Element element) { foreach (var pair in element.Attributes) { _builder.Append(' '); _builder.Append(pair.Key); var value = pair.Value; if (value == null) continue; _builder.Append('='); _builder.Append('"'); _builder.Append(HttpUtility.HtmlAttributeEncode(value)); _builder.Append('"'); } } private void PrintInlineChildNodes(Element element) { foreach (var child in element.GetLightChildren()) { PrintInlineNode(child); } } private void PrintInlineNode(Node node) { if (node is TextNode text) { _builder.Append(text.Data); } else if (node is Element element) { PrintInlineElement(element); } } private void PrintRegularElement(Element element, int indent) { Indent(indent); PrintOpeningTag(element); _builder.AppendLine(); if (!HtmlReference.IsSelfClosingTag(element.TagName)) { PrintChildNodes(element, indent + 1); PrintClosingTag(element, indent); } _builder.AppendLine(); } private void PrintChildNodes(Element element, int indent) { foreach (var child in element.GetLightChildren()) { PrintElement((Element)child, indent); } } private void PrintClosingTag(Element element, int indent) { Indent(indent); _builder.Append('<'); _builder.Append('/'); _builder.Append(element.TagName); _builder.Append('>'); } public override string ToString() { return _builder.ToString(); } } } ================================================ FILE: src/LaraUI/DOM/DomSurgeon.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Integrative.Lara { internal sealed class DomSurgeon { private readonly Element _parent; public DomSurgeon(Element parent) { _parent = parent; } #region Public methods public void Append(Node child) { var previous = BeforeOperation(child); AppendInternal(child); AfterOperation(child, previous); } public void InsertChildBefore(Node reference, Node child) { var previous = BeforeOperation(child); InsertChild(reference, 0, child); AfterOperation(child, previous); } public void InsertChildAfter(Node reference, Node child) { var previous = BeforeOperation(child); InsertChild(reference, 1, child); AfterOperation(child, previous); } public void InsertChildAt(int index, Node child) { var previous = BeforeOperation(child); InsertChild(index, child); AfterOperation(child, previous); } public void Remove(Node child) { RemoveInternal(child); AfterOperation(child, _parent); } public void RemoveAt(int index) { var child = _parent.GetChildAt(index); RemoveInternal(index); AfterOperation(child, _parent); } public void ClearChildren() { var list = new List(_parent.Children); ClearChildrenInternal(); foreach (var node in list) { AfterOperation(node, _parent); } } #endregion #region Connect and disconnect private static Element? BeforeOperation(Node child) { return child.ParentElement; } private static void AfterOperation(Node node, Node? previousParent) { if (node is Element child) { AfterOperationInternal(child, previousParent); } } private static void AfterOperationInternal(Element child, Node? previousParent) { var previousDocument = GetPreviousDocument(previousParent); if (previousDocument == child.Document) { if (previousDocument != null) { child.NotifyMove(); } } else if (child.Document == null) { child.NotifyDisconnect(); } else if (previousDocument == null) { child.NotifyConnect(); } else { child.NotifyAdopted(); } } private static Document? GetPreviousDocument(Node? previousParent) { return previousParent?.Document; } #endregion #region Operations private void AppendInternal(Node child) { PreventCycles(child); UpdateDocumentMappings(child); UpdateChildParentLinks(child); } private void InsertChild(Node reference, int offset, Node child) { VerifyParentContainsChild(reference); PreventCycles(child); UpdateDocumentMappings(child); UpdateChildParentLinks(reference, offset, child); } private void InsertChild(int index, Node child) { PreventCycles(child); UpdateDocumentMappings(child); UpdateChildParentLinks(index, child); } private void VerifyParentContainsChild(Node reference) { if (!_parent.ContainsChild(reference)) { throw new InvalidOperationException(Resources.ReferenceNodeNotFound); } } private void RemoveInternal(Node child) { if (child.ParentElement != _parent) { throw new InvalidOperationException(Resources.NodeNotFoundInsideParent); } if (child is Element childElement) { RemoveElementDelta.Enqueue(childElement); RemoveInternalCommon(child); } else { var index = _parent.GetChildNodePosition(child); RemoveInternalCommon(child); NodeRemovedDelta.Enqueue(_parent, index); } } private void RemoveInternal(int index) { var child = _parent.GetChildAt(index); RemoveInternalCommon(child); NodeRemovedDelta.Enqueue(_parent, index); } private void ClearChildrenInternal() { while (_parent.ChildCount > 0) { var child = _parent.GetChildAt(_parent.ChildCount - 1); RemoveInternalCommon(child); } ClearChildrenDelta.Enqueue(_parent); } private void RemoveInternalCommon(Node child) { if (child is Element && _parent.Document != null) { var list = CollectNodes(child); RemoveFromPreviousDocument(list, _parent.Document); } _parent.OnChildRemoved(child); child.ParentElement = null; child.UpdateSlotted(); } #endregion #region Prevent cycles in DOM tree private void PreventCycles(Node child) { if (child is Element element && _parent.DescendsFrom(element)) { throw new InvalidOperationException(Resources.CannotAddInsideItself); } } #endregion #region Update parent document and ID maps private void UpdateDocumentMappings(Node child) { var newToDocument = NewToDocument(child, out var newDocument); var leavingPrevious = LeavingPrevious(child, out var previous); if (!newToDocument && !leavingPrevious) return; var list = CollectNodes(child); if (leavingPrevious) { RemoveFromPreviousDocument(list, previous!); } if (!newToDocument) return; PreventDuplicateIds(list); AddToDocument(list, newDocument!); } private bool NewToDocument(Node child, [NotNullWhen(true)] out Document? newDocument) { newDocument = _parent.Document; return newDocument != null && child.Document != newDocument; } private bool LeavingPrevious(Node child, [NotNullWhen(true)] out Document? previousDocument) { previousDocument = child.Document; return previousDocument != null && previousDocument != _parent.Document; } private static List CollectNodes(Node child) { var list = new List(); CollectElements(list, child); return list; } private static void CollectElements(ICollection list, Node node) { list.Add(node); if (node is not Element element) return; foreach (var child in element.GetAllDescendants()) { CollectElements(list, child); } } private void PreventDuplicateIds(IEnumerable list) { var document = _parent.Document; var hash = new HashSet(); foreach (var node in list) { if (node is not Element element || string.IsNullOrEmpty(element.Id)) continue; var id = element.Id; if (hash.Contains(id) || DuplicateIdInDocument(document, id)) { throw DuplicateElementIdException.Create(id); } hash.Add(id); } } private static bool DuplicateIdInDocument(Document? document, string id) { return document != null && document.TryGetElementById(id, out _); } private static void AddToDocument(IEnumerable list, Document document) { foreach (var node in list) { node.Document = document; if (node is Element element) { document.OnElementAdded(element); } } } private static void RemoveFromPreviousDocument(IEnumerable list, Document document) { foreach (var node in list) { if (node is Element element) { document.OnElementRemoved(element); } node.Document = null; } } #endregion #region Update Children collections and ParentElement reference private void UpdateChildParentLinks(Node child) { child.ParentElement?.OnChildRemoved(child); _parent.OnChildAppend(child); child.ParentElement = _parent; child.UpdateSlotted(); NodeAddedDelta.Enqueue(child); } private void UpdateChildParentLinks(Node reference, int offset, Node child) { var index = _parent.GetChildNodePosition(reference) + offset; UpdateChildParentLinks(index, child); } private void UpdateChildParentLinks(int index, Node child) { child.ParentElement?.OnChildRemoved(child); _parent.OnChildInsert(index, child); child.ParentElement = _parent; child.UpdateSlotted(); NodeInsertedDelta.Enqueue(child, index); } #endregion } } ================================================ FILE: src/LaraUI/DOM/DuplicateElementIdException.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Exception thrown when adding a duplicate element ID into a document /// /// public class DuplicateElementIdException : InvalidOperationException { /// /// Initializes a new instance of the class. /// // ReSharper disable once UnusedMember.Global public DuplicateElementIdException() { } /// /// Initializes a new instance of the class. /// /// The message that describes the error. // ReSharper disable once MemberCanBePrivate.Global public DuplicateElementIdException(string message) : base(message) { } /// /// Initializes a new instance of the class. /// /// The message. /// The inner. public DuplicateElementIdException(string message, Exception inner) : base(message, inner) { } /// /// Creates a DuplicateElementId exception /// /// The identifier that is duplicate. /// Exception created public static DuplicateElementIdException Create(string id) { var message = $"Duplicate element Id: {id}"; return new DuplicateElementIdException(message); } } } ================================================ FILE: src/LaraUI/DOM/Element.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Integrative.Lara { /// /// An Element node inside an HTML5 document /// /// public abstract class Element : Node { private readonly Attributes _attributes; private readonly List _children; internal Dictionary Events { get; } private string _id; /// /// Element's tag name /// /// /// The element's tag name /// public string TagName { get; } /// /// Creates an element /// /// Element's tag name. /// Element created public static Element Create(string tagName) => ElementFactory.CreateElement(tagName); /// /// Creates an element /// /// Element's tag name. /// The identifier. /// Element created public static Element Create(string tagName, string id) => ElementFactory.CreateElement(tagName, id); /// /// Creates a namespace-specific HTML5 element (e.g. SVG elements) /// /// The namespace of the element /// Element's tag name. /// Element created // ReSharper disable once InconsistentNaming public static Element CreateNS(string ns, string tagName) => ElementFactory.CreateElementNs(ns, tagName); /// /// Constructor /// /// element tag protected Element(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) { tagName = GetDefaultTagName(GetType()); } TagName = tagName.ToLowerInvariant(); _id = GlobalSerializer.GenerateElementId(); _attributes = new Attributes(this); _children = new List(); Events = new Dictionary(); } /// /// Constructor /// protected Element() : this("") { } /// /// Returns a default/suggested tag name for a type /// /// object type /// default/suggested tag name public static string GetDefaultTagName(Type type) { var name = type.FullName ?? throw new ArgumentException("Invalid type name"); var tail = name.Replace('.', '-').ToLowerInvariant(); return $"x-{tail}"; } /// /// Gets the type of the node. /// /// /// The type of the node. /// public sealed override NodeType NodeType => NodeType.Element; /// /// Converts to string. /// /// /// A that represents this instance. /// public override string ToString() { var suffix = Class; if (string.IsNullOrEmpty(suffix)) { suffix = string.Empty; } else { suffix = " " + suffix; } return string.IsNullOrEmpty(_id) ? TagName : $"{TagName} #{_id}{suffix}"; } /// /// Returns the ID by assigning one if needed /// /// [Obsolete("Not needed anymore")] [EditorBrowsable(EditorBrowsableState.Never)] public string EnsureElementId() => Id; #region Attributes /// /// Gets or sets the identifier. /// /// /// The identifier. /// public string Id { get => _id; set { if (value == _id) { return; } if (string.IsNullOrWhiteSpace(value)) { throw new InvalidOperationException("Element IDs cannot be empty"); } Document?.NotifyChangeId(this, _id, value); _attributes.SetAttributeLower("id", value); _id = value; } } /// /// Sets an attribute and its value. /// /// The name of the attribute. /// The value of the attribute. public void SetAttribute(string attributeName, string? attributeValue) { attributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); SetAttributeLower(attributeName.ToLowerInvariant(), attributeValue); } internal void SetAttributeLower(string nameLower, string? value) { if (nameLower == "id") { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("Element IDs cannot be empty"); } Id = value; } else { _attributes.SetAttributeLower(nameLower, value); } } internal void ToggleAttributeLower(string nameLower, bool value) { if (value) { SetAttributeLower(nameLower, ""); } else { RemoveAttribute(nameLower); } } /// /// Determines whether the element has the given attribute /// /// The name of the attribute. /// /// true if the element has the specified attribute; otherwise, false. /// public bool HasAttribute(string attributeName) { attributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); return _attributes.HasAttribute(attributeName); } internal bool HasAttributeLower(string nameLower) => _attributes.HasAttributeLower(nameLower); /// /// Gets the value of an attribute. /// /// The name of the attribute /// Value of the attribute public string? GetAttribute(string attributeName) { attributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); return _attributes.GetAttribute(attributeName); } internal string? GetAttributeLower(string nameLower) => _attributes.GetAttributeLower(nameLower); /// /// Adds or removes a flag attribute /// /// Attribute's name /// true to add, false to remove public void SetFlagAttribute(string attributeName, bool value) { attributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); _attributes.SetFlagAttributeLower(attributeName.ToLowerInvariant(), value); } internal void SetFlagAttributeLower(string nameLower, bool value) => _attributes.SetFlagAttributeLower(nameLower, value); /// /// Removes an attribute. /// /// The name of the attribute. public void RemoveAttribute(string attributeName) { attributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName)); attributeName = attributeName.ToLowerInvariant(); if (attributeName == "id") { throw new InvalidOperationException("Cannot remove element ID attribute"); } _attributes.RemoveAttributeLower(attributeName); } internal int? GetIntAttribute(string nameLower) { if (int.TryParse(GetAttributeLower(nameLower), out var value)) { return value; } return null; } internal void SetIntAttribute(string nameLower, int? value) { if (value == null) { _attributes.RemoveAttributeLower(nameLower); } else { var text = ((int)value).ToString(CultureInfo.InvariantCulture); SetAttributeLower(nameLower, text); } } /// /// Gets the attributes. /// /// /// The attributes. /// public IEnumerable> Attributes => _attributes; /// /// Determines whether the 'class' attribute contains the specified class. /// /// Name of the class. /// /// true if the specified class is found; otherwise, false. /// public bool HasClass(string className) => ClassEditor.HasClass(Class, className); /// /// Adds the given class name to the 'class' attribute. /// /// Name of the class. public void AddClass(string className) => Class = ClassEditor.AddClass(Class, className); /// /// Removes the given class name from the 'class' attribute. /// /// Name of the class. public void RemoveClass(string className) => Class = ClassEditor.RemoveClass(Class, className); /// /// Adds or removes the given class name from the 'class' attribute. /// /// Name of the class. /// true to add the class, false to remove. public void ToggleClass(string className, bool value) => Class = ClassEditor.ToggleClass(Class, className, value); /// /// Toggles (adds or removes) the class passed in parameters /// /// Class name to toggle public void ToggleClass(string className) => Class = ClassEditor.ToggleClass(Class, className); internal void NotifyValue(string value) => _attributes.NotifyValue(value); internal void NotifyChecked(bool value) => _attributes.NotifyChecked(value); internal void NotifySelected(bool value) => _attributes.NotifySelected(value); #endregion #region Global attributes /// /// The 'accesskey' HTML5 attribute. /// /// /// The access key. /// public virtual string? AccessKey { get => GetAttributeLower("accesskey"); set => SetAttributeLower("accesskey", value); } /// /// The 'autocapitalize' HTML5 attribute. /// /// /// The automatic capitalize. /// // ReSharper disable once UnusedMember.Global public virtual string? AutoCapitalize { get => GetAttributeLower("autocapitalize"); set => SetAttributeLower("autocapitalize", value); } /// /// The 'class' HTML5 attribute. /// /// /// The class. /// public virtual string? Class { get => GetAttributeLower("class"); set => SetAttributeLower("class", value); } /// /// The 'contenteditable' HTML5 attribute. /// /// /// The content editable. /// public virtual string? ContentEditable { get => GetAttributeLower("contenteditable"); set => SetAttributeLower("contenteditable", value); } /// /// The 'contextmenu' HTML5 attribute. /// /// /// The context menu. /// // ReSharper disable once UnusedMember.Global public virtual string? ContextMenu { get => GetAttributeLower("contextmenu"); set => SetAttributeLower("contextmenu", value); } /// /// The 'dir' HTML5 attribute. /// /// /// The dir. /// public virtual string? Dir { get => GetAttributeLower("dir"); set => SetAttributeLower("dir", value); } /// /// The 'draggable' HTML5 attribute. /// /// /// The draggable. /// public virtual string? Draggable { get => GetAttributeLower("draggable"); set => SetAttributeLower("draggable", value); } /// /// The 'dropzone' HTML5 attribute. /// /// /// The drop zone. /// public virtual string? DropZone { get => GetAttributeLower("dropzone"); set => SetAttributeLower("dropzone", value); } /// /// The 'hidden' HTML5 attribute. /// /// /// true if hidden; otherwise, false. /// public virtual bool Hidden { get => HasAttributeLower("hidden"); set => SetFlagAttributeLower("hidden", value); } /// /// The 'inputmode' HTML5 attribute. /// /// /// true if [input mode]; otherwise, false. /// // ReSharper disable once UnusedMember.Global public virtual bool InputMode { get => HasAttributeLower("inputmode"); set => SetFlagAttributeLower("inputmode", value); } /// /// The 'lang' HTML5 attribute /// /// /// The language. /// public virtual string? Lang { get => GetAttributeLower("lang"); set => SetAttributeLower("lang", value); } /// /// The 'spellcheck' HTML5 attribute. /// /// /// The spellcheck. /// public virtual string? Spellcheck { get => GetAttributeLower("spellcheck"); set => SetAttributeLower("spellcheck", value); } /// /// The 'style' HTML5 attribute. /// /// /// The style. /// public virtual string? Style { get => GetAttributeLower("style"); set => SetAttributeLower("style", value); } /// /// The 'tabindex' HTML5 attribute. /// /// /// The index of the tab. /// public virtual string? TabIndex { get => GetAttributeLower("tabindex"); set => SetAttributeLower("tabindex", value); } /// /// The 'title' HTML5 attribute. /// /// /// The title. /// public virtual string? Title { get => GetAttributeLower("title"); set => SetAttributeLower("title", value); } /// /// The 'translate' HTML5 attribute. /// /// /// The translate. /// public virtual string? Translate { get => GetAttributeLower("translate"); set => SetAttributeLower("translate", value); } #endregion #region DOM tree queries /// /// Element's child nodes /// /// /// The children. /// public IEnumerable Children { get => _children; set { if (_children.Equals(value)) return; var list = value.ToArray(); BeginUpdate(); ClearChildren(); AppendChild(list); EndUpdate(); } } /// /// Returns the number of child nodes. /// /// /// The child count. /// public int ChildCount => _children.Count; /// /// Gets the child at the given index. /// /// The index of the child. /// Child node at the given index public Node GetChildAt(int index) => _children[index]; /// /// Searches for a direct child node and returns its index in the list of child nodes. /// /// The node to search for. /// The 0-based child index, or -1 if not found. public int GetChildNodePosition(Node node) { var index = 0; foreach (var child in _children) { if (child == node) { return index; } index++; } return -1; } /// /// Searches for a direct child element and returns its index in the list of child elements. /// /// The element to search for. /// The 0-based child index, or -1 if not found. public int GetChildElementPosition(Element element) { var index = 0; foreach (var child in _children) { if (child == element) { return index; } if (child is Element) { index++; } } return -1; } /// /// Verifies whether an element descends from another. /// /// The element that may be a parent. /// True if the current element descends from the given element. public bool DescendsFrom(Element? element) { if (this == element) { return true; } if (element == null || ParentElement == null) { return false; } return ParentElement == element || ParentElement.DescendsFrom(element); } /// /// Determines whether the node is a direct child. /// /// The node. /// /// true if the specified node is a child; otherwise, false. /// public bool ContainsChild(Node node) { return _children.Contains(node); } #endregion #region DOM operations internal void OnChildRemoved(Node child) { _children.Remove(child); } internal void OnChildAppend(Node child) { _children.Add(child); } internal void OnChildInsert(int index, Node child) { _children.Insert(index, child); } /// /// Appends a child node. /// /// The node to append. public void AppendChild(params Node[] nodes) { BeginUpdate(); var append = new DomSurgeon(this); foreach (var node in nodes) { append.Append(node); OnChildAdded(node); } EndUpdate(); } /// /// Appends text inside an element. /// When the element's last child is a text node, the text is appended to that node. /// Otherwise, a new child text node is added to the element. /// /// Text of the node public void AppendText(string text) { AppendEncode(text, true); } /// /// Appends raw HTML inside an element. The HTML won't be verified or parsed by Lara. /// /// raw HTML public void AppendData(string data) { AppendEncode(data, false); } internal void AppendEncode(string? data, bool encode) { var count = _children.Count; if (count > 0 && _children[count - 1] is TextNode node) { node.AppendEncode(data, encode); } else { AppendChild(new TextNode(data, encode)); } } /// /// Inserts a child node, right before the specified node. /// /// The node that is before. /// The node to insert. public void InsertChildBefore(Node before, Node node) { var append = new DomSurgeon(this); append.InsertChildBefore(before, node); OnChildAdded(node); } /// /// Inserts a child node, right after the specified node. /// /// The node that is after. /// The node to insert. public void InsertChildAfter(Node after, Node node) { var append = new DomSurgeon(this); append.InsertChildAfter(after, node); OnChildAdded(node); } /// /// Inserts a node at a given index position /// /// 0-based index position /// Node to insert public void InsertChildAt(int index, Node node) { var append = new DomSurgeon(this); append.InsertChildAt(index, node); OnChildAdded(node); } /// /// Removes a child. /// /// The child. public void RemoveChild(Node child) { var remover = new DomSurgeon(this); remover.Remove(child); } /// /// Removes the child at the given index position /// /// Index position public void RemoveAt(int index) { var remover = new DomSurgeon(this); remover.RemoveAt(index); } /// /// Removes all child nodes. /// public void ClearChildren() { var remover = new DomSurgeon(this); remover.ClearChildren(); OnPropertyChanged(nameof(Children)); } /// /// Removes this node from its parent. /// /// Cannot remove from parent, the node has no parent element already public void Remove() { if (ParentElement == null) { throw new InvalidOperationException(Resources.CannotRemoveNoParent); } ParentElement.RemoveChild(this); } /// /// Swaps two child nodes within the element /// /// Index of 1st node /// Index of 2nd node public void SwapChildren(int index1, int index2) { if (index1 == index2) { return; } var temp = _children[index1]; _children[index1] = _children[index2]; _children[index2] = temp; SwapChildrenDelta.Enqueue(this, index1, index2); } internal virtual void NotifyValue(ElementEventValue entry) { } private protected virtual void OnChildAdded(Node child) { OnPropertyChanged(nameof(Children)); } /// /// Occurs when the element is connected to the document's DOM. /// protected virtual void OnConnect() { } /// /// Occurs when the element is disconnected from the document's DOM. /// protected virtual void OnDisconnect() { } /// /// Occurs when the element is moved from one document to another. /// protected virtual void OnAdopted() { } /// /// Occurs when the element or one of its containing elements is moved within the same document's DOM. /// protected virtual void OnMove() { } internal void NotifyConnect() { FlushEvents(); OnConnect(); foreach (var child in GetNotifyList()) { child.NotifyConnect(); } } internal void NotifyDisconnect() { OnDisconnect(); foreach (var child in GetNotifyList()) { child.NotifyDisconnect(); } } internal void NotifyAdopted() { OnAdopted(); foreach (var child in GetNotifyList()) { child.NotifyAdopted(); } } internal void NotifyMove() { OnMove(); foreach (var child in GetNotifyList()) { child.NotifyMove(); } } internal virtual IEnumerable GetNotifyList() { foreach (var node in _children) { if (node is Element child) { yield return child; } } } #endregion #region Generate Delta content internal override ContentNode GetContentNode() { if (!Render) { return GetUnrenderedContent(); } return GetRenderedContent(); } private ContentNode GetUnrenderedContent() { if (IsPrintable && IsSlotted) { return new ContentPlaceholder(Id); } else { return new ContentArrayNode { Nodes = new List() }; } } private ContentNode GetRenderedContent() { var list = new List(GetLightSlotted()); if (list.Count == 1 && list[0] == this) { return GetElementContent(); } return GetArrayContent(list); } private ContentNode GetArrayContent(List list) { var array = new ContentArrayNode { Nodes = new List() }; foreach (var node in list) { array.Nodes.Add(node.GetContentNode()); } return array; } private ContentElementNode GetElementContent() { return new ContentElementNode { TagName = TagName, NS = GetAttributeLower("xlmns"), Attributes = CopyAttributes(), Children = CopyLightChildren() }; } private List CopyAttributes() { var list = new List(); foreach (var pair in _attributes) { list.Add(new ContentAttribute { Attribute = pair.Key, Value = pair.Value }); } return list; } private List CopyLightChildren() { var list = new List(); foreach (var child in GetLightChildren()) { list.Add(child.GetContentNode()); } return list; } #endregion #region Subscribe to events internal Task NotifyEvent(string eventName) { if (Events.TryGetValue(eventName, out var settings) && settings.Handler != null) { return settings.Handler(); } return Task.CompletedTask; } /// /// Registers an event and associates code to execute. /// /// The event's settings. public void On(EventSettings settings) { settings = settings ?? throw new ArgumentNullException(nameof(settings)); settings.Verify(); RemoveEvent(settings.EventName); Events.Add(settings.EventName, settings); if (Document != null) { SubscribeDelta.Enqueue(this, settings); } } /// /// Registers an event and associates code to execute. /// /// Name of the event. /// The handler to execute. public void On(string eventName, Func? handler) { eventName = eventName ?? throw new ArgumentNullException(nameof(eventName)); if (handler == null) { RemoveEvent(eventName); } else { On(new EventSettings { EventName = eventName, Handler = handler }); } } /// /// Registers an event and associates code to execute. /// /// Name of the event. /// The handler to execute. public void On(string eventName, Action? handler) { eventName = eventName ?? throw new ArgumentNullException(nameof(eventName)); if (handler == null) { RemoveEvent(eventName); } else { On(new EventSettings { EventName = eventName, Handler = () => { handler(); return Task.CompletedTask; } }); } } private void RemoveEvent(string eventName) { if (!Events.ContainsKey(eventName)) return; Events.Remove(eventName); Document?.Enqueue(new UnsubscribeDelta { ElementId = Id, EventName = eventName }); } private void FlushEvents() { foreach (var settings in Events.Values) { SubscribeDelta.Enqueue(this, settings); } } #endregion #region Binding private HashSet? _subscriptions; private ChildrenBindingSubscription? _childrenBinding; private int _applyingBinding; private const int MaxApplyLevel = 5; internal void AddSubscription(INotifyPropertyChanged source, Action action) { _subscriptions ??= new HashSet(); action(); _subscriptions.Add(new BindingSubscription(source, (_, _) => { if (_applyingBinding > MaxApplyLevel) { throw new InvalidOperationException("Cycle detected when applying updates on bindings"); } _applyingBinding++; action(); _applyingBinding--; })); } internal void SubscribeChildren( INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler) { _childrenBinding?.Unsubscribe(); _childrenBinding = new ChildrenBindingSubscription(handler, source); } private void ClearSubscriptions() { _childrenBinding?.Unsubscribe(); if (_subscriptions == null) return; foreach (var item in _subscriptions) { item.Unsubscribe(); } _subscriptions.Clear(); } /// /// Removes all bindings for the element /// public void UnbindAll() { ClearSubscriptions(); } /// /// Clears all child nodes and replaces them with raw HTML code. The HTML won't be parsed by Lara. /// /// raw HTML public void SetInnerData(string data) { SetInnerEncode(data, false); } internal void SetInnerEncode(string? value, bool encode) { if (_children.Count == 1 && _children[0] is TextNode node) { if (encode) { node.SetEncodedText(value); } else { node.Data = value; } } else { ClearChildren(); AppendEncode(value, encode); } } internal override string? GetNodeInnerText() { if (ChildCount == 0) { return string.Empty; } if (ChildCount == 1) { return GetChildAt(0).InnerText; } var builder = new StringBuilder(); AppendNodeInnerText(builder); return builder.ToString(); } internal override void AppendNodeInnerText(StringBuilder builder) { foreach (var child in Children) { child.AppendNodeInnerText(builder); } } internal override void SetNodeInnerText(string? value) { SetInnerEncode(value, true); } #endregion #region Component-related private bool _render = true; /// /// Set to false to prevent the element from rendering on the client's document /// public bool Render { get => _render; set { if (value == _render) return; SetProperty(ref _render, value); if (Document == null || !IsSlotted) return; var light = GetLightSlotted(); if (_render) { RenderDelta.Enqueue(Document, light); } else { UnRenderDelta.Enqueue(Document, light); } } } internal virtual IEnumerable GetLightSlotted() { yield return this; } internal IEnumerable GetLightChildren() { foreach (var node in Children) { if (node is Element childElement) { foreach (var light in childElement.GetLightSlotted()) { yield return light; } } else { yield return node; } } } internal virtual IEnumerable GetAllDescendants() { return Children; } internal virtual void AttributeChanged(string attribute, string? value) { OnPropertyChanged(attribute); } internal bool TryGetQueue([NotNullWhen(true)] out Document? document) { return TryGetEvents(out document) && document != null && document.QueueingEvents; } internal bool TryGetEvents([NotNullWhen(true)] out Document? document) { document = Document; return document != null && IsSlotted && IsPrintable && Render; } #endregion #region Other methods /// /// Focuses this element. /// // ReSharper disable once VirtualMemberNeverOverridden.Global public virtual void Focus() { if (Document == null) { throw new InvalidOperationException(Resources.FocusDisconnected); } FocusDelta.Enqueue(this); } /// /// Calculates and returns the HTML code of the element /// /// HTML code public string GetHtml() { var writer = new DocumentWriter(); writer.PrintElement(this, 0); return writer.ToString(); } #endregion } } ================================================ FILE: src/LaraUI/DOM/ElementFactory.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; namespace Integrative.Lara { internal static class ElementFactory { private readonly static Dictionary> _Creators = new Dictionary> { { "a", () => new HtmlAnchorElement() }, { "body", () => new HtmlBodyElement() }, { "button", () => new HtmlButtonElement() }, { "colgroup", () => new HtmlColGroupElement() }, { "div", () => new HtmlDivElement() }, { "h1", () => new HtmlHeadingElement(1) }, { "h2", () => new HtmlHeadingElement(2) }, { "h3", () => new HtmlHeadingElement(3) }, { "h4", () => new HtmlHeadingElement(4) }, { "h5", () => new HtmlHeadingElement(5) }, { "h6", () => new HtmlHeadingElement(6) }, { "head", () => new HtmlHeadElement() }, { "img", () => new HtmlImageElement() }, { "input", () => new HtmlInputElement() }, { "label", () => new HtmlLabelElement() }, { "link", () => new HtmlLinkElement() }, { "li", () => new HtmlLiElement() }, { "meta", () => new HtmlMetaElement() }, { "meter", () => new HtmlMeterElement() }, { "option", () => new HtmlOptionElement() }, { "optgroup", () => new HtmlOptionGroupElement() }, { "ol", () => new HtmlOlElement() }, { "p", () => new HtmlParagraphElement() }, { "script", () => new HtmlScriptElement() }, { "select", () => new HtmlSelectElement() }, { "slot", () => new Slot() }, { "span", () => new HtmlSpanElement() }, { "table", () => new HtmlTableElement() }, { "tbody", () => new HtmlTableSectionElement(HtmlTableSectionType.Body) }, { "thead", () => new HtmlTableSectionElement(HtmlTableSectionType.Head) }, { "tfoot", () => new HtmlTableSectionElement(HtmlTableSectionType.Foot) }, { "td", () => new HtmlTableCellElement() }, { "th", () => new HtmlTableHeaderElement() }, }; public static Element CreateElement(string tagName) { tagName = VerifyTagName(tagName); if (_Creators.TryGetValue(tagName, out var creator)) { return creator(); } if (LaraUI.Context.Application.TryGetComponent(tagName, out var type)) { return (Element)Activator.CreateInstance(type); } return new GenericElement(tagName); } private static string VerifyTagName(string tagName) { if (string.IsNullOrEmpty(tagName)) { throw new ArgumentException(Resources.InvalidTagName); } if (tagName.Contains(' ', StringComparison.InvariantCulture)) { throw new ArgumentException(Resources.TagNameSpaces); } return tagName.ToLowerInvariant(); } public static Element CreateElement(string tagName, string id) { var element = CreateElement(tagName); element.Id = id; return element; } public static Element CreateElementNs(string ns, string tagName) { var element = CreateElement(tagName); element.SetAttribute("xlmns", ns); return element; } } } ================================================ FILE: src/LaraUI/DOM/EventSettings.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Propagation options for client events /// public enum PropagationType { /// /// Uses the 'StopPropagation' option /// Default = 0, /// /// Prevents the event to bubble up to parent elements /// StopPropagation = 1, /// /// Prevents the event to bubble up to parent elements or any other handler /// StopImmediatePropagation = 2, /// /// Allows event propagation /// AllowAll = 3 } /// /// Specifies settings to declare an event and associate execution code to it. /// public sealed class EventSettings { /// /// Name of the HTML5 event (e.g. 'click') /// /// /// The name of the event. /// public string EventName { get; set; } = string.Empty; /// /// Specifies an interval in millisenconds to postpone and hold an event while it gets triggered repeatedly. /// The resulting 'debounced' event will execute only after it has not been triggered for the interval specified. /// public int DebounceInterval { get; set; } /// /// Block UI input while the event is executing? Default is true /// /// /// true if block; otherwise, false. /// public bool Block { get; set; } = true; /// /// Gets or sets the options for blocking the UI. /// /// /// The block options. /// public BlockOptions? BlockOptions { get; set; } /// /// Gets or sets the code to execute for the event. /// /// /// The handler. /// public Func? Handler { get; set; } /// /// Long-running events use websockets and can flush partial progress to the client. /// /// /// true if [long running]; otherwise, false. /// public bool LongRunning { get; set; } /// /// When set to true, the client will send the files from input elements of type 'file' /// public bool UploadFiles { get; set; } /// /// Defines JavaScript code to execute in order to determine if the event should trigger. /// When this property is set, the event is trigger only if the expression evaluates to true. /// Example: "event.keyCode === 13" /// public string? EvalFilter { get; set; } /// /// Specifies propagation options for client events /// public PropagationType Propagation { get; set; } /// /// Execute the event.PreventDefault() method to disable default behavior /// public bool PreventDefault { get; set; } internal void Verify() { if (string.IsNullOrEmpty(EventName)) { throw new ArgumentException(Resources.EventNameNull); } } } } ================================================ FILE: src/LaraUI/DOM/GlobalSerializer.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Globalization; using System.Threading; namespace Integrative.Lara { internal static class GlobalSerializer { private static long _counter; public static string GenerateElementId() { Interlocked.Increment(ref _counter); return "_g" + _counter.ToString("X", CultureInfo.InvariantCulture); } } } ================================================ FILE: src/LaraUI/DOM/HtmlReference.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Collections.Generic; namespace Integrative.Lara { internal static class HtmlReference { private static readonly HashSet _SelfClosingTags; private static readonly HashSet _DoesRequireId; static HtmlReference() { _SelfClosingTags = new HashSet { "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr" }; _DoesRequireId = new HashSet { "input", "textarea", "select", "button", "option" }; } public static bool IsSelfClosingTag(string tagNameLower) => _SelfClosingTags.Contains(tagNameLower); public static bool RequiresId(string tagNameLower) => _DoesRequireId.Contains(tagNameLower); } } ================================================ FILE: src/LaraUI/DOM/MessageRegistry.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Parameters for client messages /// public class MessageEventArgs : EventArgs { /// /// Body of message sent from JavaScript /// public string? Body { get; } internal MessageEventArgs(string? body) { Body = body; } } internal class MessageTypeRegistry { private readonly HashSet> _registry = new HashSet>(); public void Add(Func handler) { _registry.Add(handler); } public void Remove(Func handler) { _registry.Remove(handler); } public async Task RunAll(MessageEventArgs args) { var list = new List>(_registry); foreach (var handler in list) { await handler(args); } } } internal class MessageRegistry { private readonly Document _parent; private readonly Dictionary _map; public MessageRegistry(Document parent) { _parent = parent; _map = new Dictionary(); } public void Add(string messageId, Func handler) { var registry = GetRegistry(messageId); registry.Add(handler); } private MessageTypeRegistry GetRegistry(string messageId) { if (_map.TryGetValue(messageId, out var registry)) return registry; registry = new MessageTypeRegistry(); _map.Add(messageId, registry); _parent.Head.On("_" + messageId, () => { var body = LaraUI.Page.JSBridge.EventData; var args = new MessageEventArgs(body); return RunAll(messageId, args); }); return registry; } public void Remove(string messageId, Func handler) { if (_map.TryGetValue(messageId, out var registry)) { registry.Remove(handler); } } public async Task RunAll(string messageId, MessageEventArgs args) { if (_map.TryGetValue(messageId, out var registry)) { await registry.RunAll(args); } } } } ================================================ FILE: src/LaraUI/DOM/Node.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Text; namespace Integrative.Lara { /// /// Defines the types of document nodes /// public enum NodeType { /// /// Element node /// Element, /// /// Text node /// Text } /// /// An abstract class that represents a node in an HTML5 document /// public abstract class Node : BindableBase { /// /// Creates a node /// protected internal Node() { } /// /// The node's parent element. /// /// /// The parent element. /// public Element? ParentElement { get; internal set; } /// /// The node's document. This property returns null when the node is not attached to a document. /// /// /// The document. /// public Document? Document { get; internal set; } /// /// Gets the type of the node. /// /// /// The type of the node. /// public abstract NodeType NodeType { get; } internal abstract ContentNode GetContentNode(); /// /// True when the node is currently rendered in a parent document. /// public bool IsSlotted { get; internal set; } internal void UpdateSlotted() { SlottedCalculator.UpdateSlotted(this); } internal virtual bool IsPrintable => true; /// /// The InnerText property represents the text content of a node and all of its descendants. /// When setting the property, all descendants are replaced with a text node and the given text content. /// When getting the property, it retrieves the text of all descendants. /// public string? InnerText { get => GetNodeInnerText(); set => SetNodeInnerText(value); } internal abstract string? GetNodeInnerText(); internal abstract void SetNodeInnerText(string? value); internal abstract void AppendNodeInnerText(StringBuilder builder); } } ================================================ FILE: src/LaraUI/DOM/NodeExtensions.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Extensions for Node class /// public static class NodeExtensions { #region Generic wrapping of methods /// /// Executes an action on an object and returns the object itself /// /// /// /// /// object invoked public static TNode Wrap(this TNode node, params Action[] actions) { foreach (var action in actions) { action(node); } return node; } /// /// Returns the object itself /// /// type of node /// node /// node itself /// node itself public static TNode Extract(this TNode node, [NotNull] out TNode result) { node = node ?? throw new ArgumentNullException(nameof(node)); result = node; return node; } #endregion #region add children /// /// Appends multiple children and returns the element passed as parameter /// /// Type of parent /// parent /// child nodes /// public static T Child(this T element, params Node[] elements) where T : Element { element.AppendChild(elements); return element; } #endregion #region add events /// /// Registers an event and associates code to execute /// /// Element type /// Element /// Event name (e.g. "click") /// Code to execute /// Element instance public static T Event(this T element, string eventName, Action handler) where T : Element { element.On(eventName, handler); return element; } /// /// Registers an event and associates code to execute /// /// Element type /// Element /// Event name (e.g. "click") /// Code to execute /// Element instance public static T Event(this T element, string eventName, Func handler) where T : Element { element.On(eventName, handler); return element; } /// /// Registers an event and associates code to execute /// /// Element type /// Element /// Event options /// Element instance public static T Event(this T element, EventSettings options) where T : Element { element.On(options); return element; } #endregion } } ================================================ FILE: src/LaraUI/DOM/TextNode.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Text; using System.Web; namespace Integrative.Lara { /// /// A text node. /// /// public sealed class TextNode : Node { /// /// Gets the type of the node. /// /// /// The type of the node. /// public override NodeType NodeType => NodeType.Text; private string? _data; /// /// Gets or sets the data of the node. This property gets and sets the raw (unencoded) HTML text for the node. /// /// /// The data. /// public string? Data { get => _data; set { if (_data == value) return; _data = value; TextModifiedDelta.Enqueue(this); } } /// /// Initializes a new instance of the class. /// public TextNode() { } /// /// Initializes a new instance of the class. /// /// The data. /// if set to true encode. public TextNode(string? data, bool encode = true) { if (encode) { SetEncodedText(data); } else { _data = data; } } /// /// Appends text to the node /// /// Text to append public void AppendText(string text) { AppendEncode(text, true); } /// /// Appends raw HTML to the node /// /// raw HTML public void AppendData(string data) { AppendEncode(data, false); } internal void AppendEncode(string? text, bool encode) { if (encode) { text = HttpUtility.HtmlEncode(text); } if (string.IsNullOrEmpty(_data)) { _data = text; } else { _data += text; } } internal override ContentNode GetContentNode() { return new ContentTextNode { Data = _data, }; } /// /// Sets text for the node. /// /// The unencoded text to encode. public void SetEncodedText(string? unencoded) { Data = HttpUtility.HtmlEncode(unencoded); } internal override string GetNodeInnerText() { return Data == null ? string.Empty : HttpUtility.HtmlDecode(Data); } internal override void SetNodeInnerText(string? value) { SetEncodedText(value); } internal override void AppendNodeInnerText(StringBuilder builder) { builder.Append(GetNodeInnerText()); } } } ================================================ FILE: src/LaraUI/Delta/AttributeEditedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class AttributeEditedDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string Attribute { get; set; } = string.Empty; [DataMember] public string Value { get; set; } = string.Empty; public AttributeEditedDelta() : base(DeltaType.EditAttribute) { } public static void Enqueue(Element element, string attribute, string? value) { if (element.TryGetQueue(out var document)) { document.Enqueue(new AttributeEditedDelta { Attribute = attribute, ElementId = element.Id, Value = value ?? "" }); } } } } ================================================ FILE: src/LaraUI/Delta/AttributeRemovedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class AttributeRemovedDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string Attribute { get; set; } = string.Empty; public AttributeRemovedDelta() : base(DeltaType.RemoveAttribute) { } public static void Enqueue(Element element, string attribute) { if (element.TryGetQueue(out var document)) { document.Enqueue(new AttributeRemovedDelta { ElementId = element.Id, Attribute = attribute }); } } } } ================================================ FILE: src/LaraUI/Delta/BaseDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { internal enum DeltaType { Append = 1, Insert = 2, TextModified = 3, Remove = 4, EditAttribute = 5, RemoveAttribute = 6, Focus = 7, SetId = 8, SetValue = 9, SubmitJs = 10, SetChecked = 11, ClearChildren = 12, Replace = 13, ServerEvents = 14, SwapChildren = 15, Subscribe = 16, Unsubscribe = 17, RemoveElement = 18, Render = 19, UnRender = 20 } [DataContract] [KnownType(typeof(NodeAddedDelta))] [KnownType(typeof(NodeInsertedDelta))] [KnownType(typeof(TextModifiedDelta))] [KnownType(typeof(NodeRemovedDelta))] [KnownType(typeof(AttributeEditedDelta))] [KnownType(typeof(AttributeRemovedDelta))] [KnownType(typeof(FocusDelta))] [KnownType(typeof(SetIdDelta))] [KnownType(typeof(SetValueDelta))] [KnownType(typeof(SubmitJsDelta))] [KnownType(typeof(SetCheckedDelta))] [KnownType(typeof(ClearChildrenDelta))] [KnownType(typeof(ReplaceDelta))] [KnownType(typeof(ServerEventsDelta))] [KnownType(typeof(SwapChildrenDelta))] [KnownType(typeof(SubscribeDelta))] [KnownType(typeof(UnsubscribeDelta))] [KnownType(typeof(RemoveElementDelta))] [KnownType(typeof(RenderDelta))] [KnownType(typeof(UnRenderDelta))] internal abstract class BaseDelta { [DataMember] public DeltaType Type { get; set; } protected BaseDelta(DeltaType type) { Type = type; } } } ================================================ FILE: src/LaraUI/Delta/ClearChildrenDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ClearChildrenDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; public ClearChildrenDelta() : base(DeltaType.ClearChildren) { } public static void Enqueue(Element element) { if (element.TryGetQueue(out var document)) { document.Enqueue(new ClearChildrenDelta { ElementId = element.Id, }); } } } } ================================================ FILE: src/LaraUI/Delta/ContentArrayNode.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ContentArrayNode : ContentNode { [DataMember] public List? Nodes { get; set; } public ContentArrayNode() : base(ContentNodeType.Array) { } } } ================================================ FILE: src/LaraUI/Delta/ContentAttribute.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ContentAttribute { [DataMember] public string Attribute { get; set; } = string.Empty; [DataMember] public string Value { get; set; } = string.Empty; } } ================================================ FILE: src/LaraUI/Delta/ContentElementNode.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ContentElementNode : ContentNode { [DataMember] public string TagName { get; set; } = string.Empty; [DataMember] // ReSharper disable once InconsistentNaming public string? NS { get; set; } [DataMember] public List? Attributes { get; set; } [DataMember] public List? Children { get; set; } public ContentElementNode() : base(ContentNodeType.Element) { } } } ================================================ FILE: src/LaraUI/Delta/ContentNode.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { internal enum ContentNodeType { Element = 1, Text = 2, Array = 3, Placeholder = 4 } [DataContract] [KnownType(typeof(ContentTextNode))] [KnownType(typeof(ContentElementNode))] [KnownType(typeof(ContentArrayNode))] [KnownType(typeof(ContentPlaceholder))] internal abstract class ContentNode { [DataMember] public ContentNodeType Type { get; set; } protected ContentNode(ContentNodeType type) { Type = type; } } } ================================================ FILE: src/LaraUI/Delta/ContentPlaceholder.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 1/2021 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class ContentPlaceholder : ContentNode { [DataMember] public string ElementId { get; set; } = string.Empty; public ContentPlaceholder() : base(ContentNodeType.Placeholder) { } public ContentPlaceholder(string id) : this() { ElementId = id; } } } ================================================ FILE: src/LaraUI/Delta/ContentTextNode.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ContentTextNode : ContentNode { [DataMember] public string? Data { get; set; } public ContentTextNode() : base(ContentNodeType.Text) { } } } ================================================ FILE: src/LaraUI/Delta/ElementValue.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ElementEventValue { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string Value { get; set; } = string.Empty; [DataMember] public bool Checked { get; set; } public override string ToString() { return $"#{ElementId}='{Value}'{GetCheckedSuffix()}"; } private string GetCheckedSuffix() { return Checked ? " checked" : string.Empty; } } } ================================================ FILE: src/LaraUI/Delta/EventResult.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { internal enum EventResultType { Success = 0, NoSession = 1, NoElement = 2, OutOfSequence = 3 } [DataContract] internal sealed class EventResult { [DataMember] public EventResultType ResultType { get; set; } [DataMember] public List? List { get; set; } public EventResult() { } public EventResult(List list) : this() { List = list; } // ReSharper disable once InconsistentNaming public string ToJSON() { return LaraTools.Serialize(this); } } } ================================================ FILE: src/LaraUI/Delta/FocusDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class FocusDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; public FocusDelta() : base(DeltaType.Focus) { } public static void Enqueue(Element element) { element.Document?.Enqueue(new FocusDelta { ElementId = element.Id }); } } } ================================================ FILE: src/LaraUI/Delta/NodeAddedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class NodeAddedDelta : BaseDelta { [DataMember] public string ParentId { get; set; } = string.Empty; [DataMember] public ContentNode? Node { get; set; } public NodeAddedDelta() : base(DeltaType.Append) { } public static void Enqueue(Node node) { var parent = node.ParentElement; if (parent == null || !parent.TryGetQueue(out var document)) return; var parentId = parent.Id; var content = node.GetContentNode(); document.Enqueue(new NodeAddedDelta { ParentId = parentId, Node = content, }); } } } ================================================ FILE: src/LaraUI/Delta/NodeInsertedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class NodeInsertedDelta : BaseDelta { [DataMember] public string ParentElementId { get; set; } = string.Empty; [DataMember] public int Index { get; set; } [DataMember] public ContentNode? Node { get; set; } public NodeInsertedDelta() : base(DeltaType.Insert) { } public static void Enqueue(Node node, int index) { var parent = node.ParentElement; if (parent != null && parent.TryGetQueue(out var document)) { document.Enqueue(new NodeInsertedDelta { ParentElementId = parent.Id, Index = index, Node = node.GetContentNode() }); } } } } ================================================ FILE: src/LaraUI/Delta/NodeLocator.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class NodeLocator { [DataMember] public string StartingId { get; set; } = string.Empty; [DataMember] public int? ChildIndex { get; set; } public static NodeLocator FromNode(Node node) { if (node is Element element) { return new NodeLocator { StartingId = element.Id }; } var parent = node.ParentElement; if (parent == null) { throw new ArgumentException("NodeLocator from orphan non-element node"); } return new NodeLocator { StartingId = parent.Id, ChildIndex = parent.GetChildNodePosition(node) }; } } } ================================================ FILE: src/LaraUI/Delta/NodeRemovedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class NodeRemovedDelta : BaseDelta { [DataMember] public string ParentId { get; set; } = string.Empty; [DataMember] public int ChildIndex { get; set; } public NodeRemovedDelta() : base(DeltaType.Remove) { } public static void Enqueue(Element parent, int index) { if (parent.TryGetQueue(out var document)) { document.Enqueue(new NodeRemovedDelta { ParentId = parent.Id, ChildIndex = index, }); } } } } ================================================ FILE: src/LaraUI/Delta/PlugOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class PlugOptions { [DataMember(IsRequired = false, EmitDefaultValue = false)] public bool Block { get; set; } [DataMember(IsRequired = false, EmitDefaultValue = false)] public string? BlockElementId { get; set; } [DataMember(IsRequired = false, EmitDefaultValue = false)] // ReSharper disable once InconsistentNaming public string? BlockHTML { get; set; } [DataMember(IsRequired = false, EmitDefaultValue = false)] public string? BlockShownId { get; set; } [DataMember(IsRequired = false, EmitDefaultValue = false)] public bool LongRunning { get; set; } [DataMember(IsRequired = false, EmitDefaultValue = false)] public bool UploadFiles { get; set; } public PlugOptions() { } public PlugOptions(EventSettings settings) { Block = settings.Block; if (settings.BlockOptions != null) { BlockElementId = settings.BlockOptions.BlockedElementId; BlockHTML = settings.BlockOptions.ShowHtmlMessage; BlockShownId = settings.BlockOptions.ShowElementId; } LongRunning = settings.LongRunning; UploadFiles = settings.UploadFiles; } // ReSharper disable once InconsistentNaming public string ToJSON() => LaraTools.Serialize(this); } } ================================================ FILE: src/LaraUI/Delta/RemoveElementDelta.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 1/2021 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class RemoveElementDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; public RemoveElementDelta() : base(DeltaType.RemoveElement) { } public static void Enqueue(Element element) { if (element.TryGetQueue(out var document)) { document.Enqueue(new RemoveElementDelta { ElementId = element.Id }); } } } } ================================================ FILE: src/LaraUI/Delta/RenderDelta.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 1/2021 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class RenderDelta : BaseDelta { [DataMember] public NodeLocator? Locator { get; set; } [DataMember] public ContentNode? Node { get; set; } public RenderDelta() : base(DeltaType.Render) { } public static void Enqueue(Document document, IEnumerable nodes) { foreach (var node in nodes) { document.Enqueue(new RenderDelta { Locator = NodeLocator.FromNode(node), Node = node.GetContentNode() }); } } } } ================================================ FILE: src/LaraUI/Delta/ReplaceDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ReplaceDelta : BaseDelta { [DataMember] public string? Location { get; set; } public ReplaceDelta() : base(DeltaType.Replace) { } public static void Enqueue(Document document, string location) { document.Enqueue(new ReplaceDelta { Location = location }); } } } ================================================ FILE: src/LaraUI/Delta/ServerEventsDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class ServerEventsDelta : BaseDelta { public ServerEventsDelta() : base(DeltaType.ServerEvents) { } } } ================================================ FILE: src/LaraUI/Delta/SetCheckedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class SetCheckedDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public bool Checked { get; set; } public SetCheckedDelta() : base(DeltaType.SetChecked) { } public static void Enqueue(Element element, bool value) { if (element.TryGetQueue(out var document)) { document.Enqueue(new SetCheckedDelta { ElementId = element.Id, Checked = value }); } } } } ================================================ FILE: src/LaraUI/Delta/SetIdDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class SetIdDelta : BaseDelta { [DataMember] public string OldId { get; set; } = string.Empty; [DataMember] public string NewId { get; set; } = string.Empty; public SetIdDelta() : base(DeltaType.SetId) { } public static void Enqueue(Element element, string newValue) { if (element.TryGetQueue(out var document)) { document.Enqueue(new SetIdDelta { OldId = element.Id, NewId = newValue }); } } } } ================================================ FILE: src/LaraUI/Delta/SetValueDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class SetValueDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string? Value { get; set; } public SetValueDelta() : base(DeltaType.SetValue) { } public static void Enqueue(Element element, string? value) { if (element.TryGetQueue(out var document)) { document.Enqueue(new SetValueDelta { ElementId = element.Id, Value = value }); } } } } ================================================ FILE: src/LaraUI/Delta/SubmitJsDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class SubmitJsDelta : BaseDelta { [DataMember] public string Code { get; set; } = string.Empty; [DataMember(EmitDefaultValue = false)] public string? Payload { get; set; } public SubmitJsDelta() : base(DeltaType.SubmitJs) { } } } ================================================ FILE: src/LaraUI/Delta/SubscribeDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class SubscribeDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public ClientEventSettings? Settings { get; set; } [DataMember(EmitDefaultValue = false)] public int DebounceInterval { get; set; } [DataMember(EmitDefaultValue = false)] public string? EvalFilter { get; set; } public SubscribeDelta() : base(DeltaType.Subscribe) { } public static void Enqueue(Element element, EventSettings settings) { if (!element.TryGetEvents(out var document)) return; document.NotifyHasEvent(); document.Enqueue(CreateDelta(element.Id, settings)); } public static void Enqueue(Document document, EventSettings settings) { document.NotifyHasEvent(); document.Enqueue(CreateDelta(string.Empty, settings)); } private static SubscribeDelta CreateDelta(string id, EventSettings settings) { return new SubscribeDelta { ElementId = id, Settings = ClientEventSettings.CreateFrom(settings), DebounceInterval = settings.DebounceInterval, EvalFilter = settings.EvalFilter }; } } [DataContract] internal class ClientEventSettings { [DataMember] public string EventName { get; set; } = string.Empty; [DataMember(EmitDefaultValue = false)] public bool Block { get; set; } [DataMember(EmitDefaultValue = false)] public string? BlockElementId { get; set; } [DataMember(EmitDefaultValue = false)] // ReSharper disable once InconsistentNaming public string? BlockHTML { get; set; } [DataMember(EmitDefaultValue = false)] public string? BlockShownId { get; set; } [DataMember(EmitDefaultValue = false)] public string? ExtraData { get; set; } [DataMember(EmitDefaultValue = false)] public bool LongRunning { get; set; } [DataMember(EmitDefaultValue = false)] public PropagationType Propagation { get; set; } [DataMember(EmitDefaultValue = false)] public bool PreventDefault { get; set; } [DataMember(EmitDefaultValue = false)] public bool UploadFiles { get; set; } public static ClientEventSettings CreateFrom(EventSettings settings) { var client = new ClientEventSettings { Block = settings.Block, EventName = settings.EventName, LongRunning = settings.LongRunning, Propagation = settings.Propagation, PreventDefault = settings.PreventDefault, UploadFiles = settings.UploadFiles }; if (settings.BlockOptions == null) return client; client.BlockElementId = settings.BlockOptions.BlockedElementId; client.BlockHTML = settings.BlockOptions.ShowHtmlMessage; client.BlockShownId = settings.BlockOptions.ShowElementId; return client; } } } ================================================ FILE: src/LaraUI/Delta/SwapChildrenDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class SwapChildrenDelta : BaseDelta { public SwapChildrenDelta() : base(DeltaType.SwapChildren) { } [DataMember] public string ParentId { get; set; } = string.Empty; [DataMember] public int Index1 { get; set; } [DataMember] public int Index2 { get; set; } public static void Enqueue(Element parent, int index1, int index2) { if (parent.TryGetQueue(out var document)) { document.Enqueue(new SwapChildrenDelta { ParentId = parent.Id, Index1 = index1, Index2 = index2 }); } } } } ================================================ FILE: src/LaraUI/Delta/TextModifiedDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class TextModifiedDelta : BaseDelta { [DataMember] public string ParentElementId { get; set; } = string.Empty; [DataMember] public int ChildNodeIndex { get; set; } [DataMember] public string? Text { get; set; } public TextModifiedDelta() : base(DeltaType.TextModified) { } public static void Enqueue(TextNode node) { var parent = node.ParentElement; if (parent == null || !parent.TryGetQueue(out var document)) return; var index = parent.GetChildNodePosition(node); document.Enqueue(new TextModifiedDelta { ParentElementId = parent.Id, ChildNodeIndex = index, Text = node.Data }); } } } ================================================ FILE: src/LaraUI/Delta/UnRenderDelta.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 1/2021 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class UnRenderDelta : BaseDelta { [DataMember] public NodeLocator? Locator { get; set; } public UnRenderDelta() : base(DeltaType.UnRender) { } public static void Enqueue(Document document, IEnumerable nodes) { foreach (var node in nodes) { document.Enqueue(new UnRenderDelta { Locator = NodeLocator.FromNode(node) }); } } } } ================================================ FILE: src/LaraUI/Delta/UnsubscribeDelta.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal class UnsubscribeDelta : BaseDelta { [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string EventName { get; set; } = string.Empty; public UnsubscribeDelta() : base(DeltaType.Unsubscribe) { } } } ================================================ FILE: src/LaraUI/Elements/GenericElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// A generic element class for all elements that are not handled by specialized classes. /// /// public sealed class GenericElement : Element { internal GenericElement(string tagName) : base(tagName) { } } } ================================================ FILE: src/LaraUI/Elements/HtmlAnchorElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Anchor element /// [Obsolete("Use HtmlAnchorElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Anchor : HtmlAnchorElement { } /// /// The HTML5 'a' element /// /// public class HtmlAnchorElement : Element { /// /// Initializes a new instance of the class. /// public HtmlAnchorElement() : base("a") { } /// /// The 'download' HTML5 attribute. /// public bool Download { get => HasAttribute("download"); set => SetFlagAttributeLower("download", value); } /// /// The 'href' HTML5 attribute. /// public string? HRef { get => GetAttribute("href"); set => SetAttributeLower("href", value); } /// /// The 'hreflang' HTML5 attribute. /// public string? HRefLang { get => GetAttribute("hreflang"); set => SetAttributeLower("hreflang", value); } /// /// The 'media' HTML5 attribute. /// public string? Media { get => GetAttribute("media"); set => SetAttributeLower("media", value); } /// /// The 'ping' HTML5 attribute. /// public string? Ping { get => GetAttribute("ping"); set => SetAttributeLower("ping", value); } /// /// The 'referrerpolicy' HTML5 attribute. /// public string? ReferrerPolicy { get => GetAttribute("referrerpolicy"); set => SetAttributeLower("referrerpolicy", value); } /// /// The 'rel' HTML5 attribute. /// public string? Rel { get => GetAttribute("rel"); set => SetAttributeLower("rel", value); } /// /// The 'target' HTML5 attribute. /// public string? Target { get => GetAttributeLower("target"); set => SetAttributeLower("target", value); } /// /// The 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlBodyElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Body element /// [Obsolete("Use HtmlBodyElement")] [EditorBrowsable(EditorBrowsableState.Never)] public class BodyElement : HtmlBodyElement { } /// /// HTML 'body' element /// public class HtmlBodyElement : Element { /// /// Constructor /// public HtmlBodyElement() : base("body") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlButtonElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Button element /// [Obsolete("Use HtmlButtonElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Button : HtmlButtonElement { } /// /// The 'button' HTML5 element /// /// public class HtmlButtonElement : Element { /// /// Initializes a new instance of the class. /// public HtmlButtonElement() : base("button") { Type = "button"; } internal override void NotifyValue(ElementEventValue entry) { base.NotifyValue(entry); NotifyValue(entry.Value); } /// /// Gets or sets the 'autofocus' HTML5 attribute. /// public bool AutoFocus { get => HasAttributeLower("autofocus"); set => SetFlagAttributeLower("autofocus", value); } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttribute("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'name' HTML5 attribute. /// public string? Name { get => GetAttributeLower("name"); set => SetAttributeLower("name", value); } /// /// Gets or sets the 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } /// /// Gets or sets the 'value' property. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlColGroupElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// ColGroup element /// [Obsolete("Use HtmlColGroupElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class ColGroup : HtmlColGroupElement { } /// /// The 'colgroup' HTML5 element /// /// public class HtmlColGroupElement : Element { /// /// Initializes a new instance of the class. /// public HtmlColGroupElement() : base("colgroup") { } /// /// Gets or sets the 'span' HTML5 attribute. /// public int? Span { get => GetIntAttribute("span"); set => SetIntAttribute("span", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlDivElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// HTML div element /// public class HtmlDivElement : Element { /// /// Constructor /// public HtmlDivElement() : base("div") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlHeadElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Head element /// [Obsolete("Use HtmlHeadElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class HeadElement : HtmlHeadElement { } /// /// HTML 'head' element /// /// public class HtmlHeadElement : Element { /// /// Constructor /// public HtmlHeadElement() : base("head") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlHeadingElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Represents the h1..h6 tags /// public class HtmlHeadingElement : Element { /// /// Constructor /// /// public HtmlHeadingElement(int level) : base(GetLevelTag(level)) { } /// /// Returns h1..h6 for levels 1..6 /// /// /// public static string GetLevelTag(int level) { if (level < 1 || level > 6) { throw new ArgumentException("Invalid heading level"); } return $"h{level}"; } } } ================================================ FILE: src/LaraUI/Elements/HtmlImageElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Image element /// [Obsolete("Use HtmlImageElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Image : HtmlImageElement { } /// /// The 'img' HTML5 element. /// /// public class HtmlImageElement : Element { /// /// Initializes a new instance of the class. /// public HtmlImageElement() : base("img") { } /// /// Gets or sets the 'alt' HTML5 attribute. /// public string? Alt { get => GetAttributeLower("alt"); set => SetAttributeLower("alt", value); } /// /// Gets or sets the 'crossorigin' HTML5 attribute. /// public string? CrossOrigin { get => GetAttributeLower("crossorigin"); set => SetAttributeLower("crossorigin", value); } /// /// Gets or sets the 'height' HTML5 attribute. /// public string? Height { get => GetAttribute("height"); set => SetAttribute("height", value); } /// /// Gets or sets the 'ismap' HTML5 attribute. /// public bool IsMap { get => HasAttribute("ismap"); set => SetFlagAttributeLower("ismap", value); } /// /// Gets or sets the 'longdesc' HTML5 attribute. /// public string? LongDesc { get => GetAttributeLower("longdesc"); set => SetAttributeLower("longdesc", value); } /// /// Gets or sets the 'src' HTML5 attribute. /// public string? Src { get => GetAttributeLower("src"); set => SetAttributeLower("src", value); } /// /// Gets or sets the 'srcset' HTML5 attribute. /// public string? SrcSet { get => GetAttributeLower("srcset"); set => SetAttributeLower("srcset", value); } /// /// Gets or sets the 'usemap' HTML5 attribute. /// public string? UseMap { get => GetAttributeLower("usemap"); set => SetAttributeLower("usemap", value); } /// /// Gets or sets the 'width' HTML5 attribute. /// public string? Width { get => GetAttribute("width"); set => SetAttribute("width", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlInputElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.ComponentModel; namespace Integrative.Lara { /// /// Input element /// [Obsolete("Use HtmlInputElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class InputElement : HtmlInputElement { } /// /// The 'input' HTML5 element /// /// public class HtmlInputElement : Element { /// /// Initializes a new instance of the class. /// public HtmlInputElement() : base("input") { } internal override void NotifyValue(ElementEventValue entry) { BeginUpdate(); base.NotifyValue(entry); NotifyValue(entry.Value); NotifyChecked(entry.Checked); EndUpdate(); ClearFiles(); } /// /// Gets or sets the 'accept' HTML5 attribute. /// public string? Accept { get => GetAttributeLower("accept"); set => SetAttributeLower("accept", value); } /// /// Gets or sets the 'alt' HTML5 attribute. /// public string? Alt { get => GetAttributeLower("alt"); set => SetAttributeLower("alt", value); } /// /// Gets or sets the 'autocomplete' HTML5 attribute. /// public string? Autocomplete { get => GetAttributeLower("autocomplete"); set => SetAttributeLower("autocomplete", value); } /// /// Gets or sets the 'autofocus' HTML5 attribute. /// public bool Autofocus { get => HasAttributeLower("autofocus"); set => SetFlagAttributeLower("autofocus", value); } /// /// Gets or sets the 'checked' HTML5 attribute. /// public bool Checked { get => HasAttributeLower("checked"); set => SetFlagAttributeLower("checked", value); } /// /// Gets or sets the 'dirname' HTML5 attribute. /// public string? Dirname { get => GetAttributeLower("dirname"); set => SetAttributeLower("dirname", value); } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttributeLower("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'height' HTML5 attribute. /// public int? Height { get => GetIntAttribute("height"); set => SetIntAttribute("height", value); } /// /// Gets or sets the 'list' HTML5 attribute. /// public string? List { get => GetAttributeLower("list"); set => SetAttributeLower("list", value); } /// /// Gets or sets the 'max' HTML5 attribute. /// public string? Max { get => GetAttributeLower("max"); set => SetAttributeLower("max", value); } /// /// Gets or sets the 'maxlength' HTML5 attribute. /// public int? MaxLength { get => GetIntAttribute("maxlength"); set => SetIntAttribute("maxlength", value); } /// /// Gets or sets the 'min' HTML5 attribute. /// public string? Min { get => GetAttributeLower("min"); set => SetAttributeLower("min", value); } /// /// Gets or sets the 'multiple' HTML5 attribute. /// public bool Multiple { get => HasAttributeLower("multiple"); set => SetFlagAttributeLower("multiple", value); } /// /// Gets or sets the 'name' HTML5 attribute. /// public string? Name { get => GetAttributeLower("name"); set => SetAttributeLower("name", value); } /// /// Gets or sets the 'pattern' HTML5 attribute. /// public string? Pattern { get => GetAttributeLower("pattern"); set => SetAttributeLower("pattern", value); } /// /// Gets or sets the 'placeholder' HTML5 attribute. /// public string? Placeholder { get => GetAttributeLower("placeholder"); set => SetAttributeLower("placeholder", value); } /// /// Gets or sets the 'readonly' HTML5 attribute. /// public bool Readonly { get => HasAttributeLower("readonly"); set => SetFlagAttributeLower("readonly", value); } /// /// Gets or sets the 'required' HTML5 attribute. /// public bool Required { get => HasAttributeLower("required"); set => SetFlagAttributeLower("required", value); } /// /// Gets or sets the 'size' HTML5 attribute. /// public int? Size { get => GetIntAttribute("size"); set => SetIntAttribute("size", value); } /// /// Gets or sets the 'src' HTML5 attribute. /// public string? Src { get => GetAttributeLower("src"); set => SetAttributeLower("src", value); } /// /// Gets or sets the 'step' HTML5 attribute. /// public string? Step { get => GetAttributeLower("step"); set => SetAttributeLower("step", value); } /// /// Gets or sets the 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } /// /// Gets or sets the 'value' HTML5 attribute. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } /// /// Gets or sets the 'width' HTML5 attribute. /// public int? Width { get => GetIntAttribute("width"); set => SetIntAttribute("width", value); } private readonly List _files = new List(); private void ClearFiles() => _files.Clear(); internal void AddFile(IFormFile file) => _files.Add(file); /// /// Collection of uploaded files for input elements with type='file' /// public IReadOnlyList Files => _files; } } ================================================ FILE: src/LaraUI/Elements/HtmlLabelElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Label element /// [Obsolete("Use HtmlLabelElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Label : HtmlLabelElement { } /// /// The 'label' HTML5 element. /// /// public class HtmlLabelElement : Element { /// /// Initializes a new instance of the class. /// public HtmlLabelElement() : base("label") { } /// /// Gets or sets the 'for' HTML5 attribute. /// public string? For { get => GetAttributeLower("for"); set => SetAttributeLower("for", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlLiElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// LI element /// [Obsolete("Use HtmlLiElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class ListItem : HtmlLiElement { } /// /// The 'li' HTML5 element /// /// public class HtmlLiElement : Element { /// /// Initializes a new instance of the class. /// public HtmlLiElement() : base("li") { } /// /// Gets or sets the 'value' property. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlLinkElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Link element /// [Obsolete("Use HtmlLinkElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Link : HtmlLinkElement { } /// /// The 'link' HTML5 element. /// /// public class HtmlLinkElement : Element { /// /// Initializes a new instance of the class. /// public HtmlLinkElement() : base("link") { } /// /// Gets or sets the 'crossorigin' HTML5 attribute. /// public string? CrossOrigin { get => GetAttributeLower("crossorigin"); set => SetAttributeLower("crossorigin", value); } /// /// Gets or sets the 'href' HTML5 attribute. /// public string? HRef { get => GetAttributeLower("href"); set => SetAttributeLower("href", value); } /// /// Gets or sets the 'hreflang' HTML5 attribute. /// public string? HRefLang { get => GetAttributeLower("hreflang"); set => SetAttributeLower("hreflang", value); } /// /// Gets or sets the 'media' HTML5 attribute. /// public string? Media { get => GetAttributeLower("media"); set => SetAttributeLower("media", value); } /// /// Gets or sets the 'rel' HTML5 attribute. /// public string? Rel { get => GetAttributeLower("rel"); set => SetAttributeLower("rel", value); } /// /// Gets or sets the 'sizes' HTML5 attribute. /// public string? Sizes { get => GetAttributeLower("sizes"); set => SetAttributeLower("sizes", value); } /// /// Gets or sets the 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlMetaElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Meta element /// [Obsolete("Use HtmlMetaElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Meta : HtmlMetaElement { } /// /// The 'meta' HTML5 element. /// /// public class HtmlMetaElement : Element { /// /// Initializes a new instance of the class. /// public HtmlMetaElement() : base("meta") { } /// /// Gets or sets the 'content' HTML5 attribute. /// public string? Content { get => GetAttributeLower("content"); set => SetAttributeLower("content", value); } /// /// Gets or sets the 'httpequiv' HTML5 attribute. /// public string? HttpEquiv { get => GetAttributeLower("http-equiv"); set => SetAttributeLower("http-equiv", value); } /// /// Gets or sets the 'name' HTML5 attribute. /// public string? Name { get => GetAttributeLower("name"); set => SetAttributeLower("name", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlMeterElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Meter element /// [Obsolete("Use HtmlMeterElement")] [EditorBrowsable(EditorBrowsableState.Never)] public class Meter : HtmlMeterElement { } /// /// The 'meter' HTML5 element. /// /// public class HtmlMeterElement : Element { /// /// Initializes a new instance of the class. /// public HtmlMeterElement() : base("meter") { } /// /// Gets or sets the 'high' HTML5 attribute. /// public int? High { get => GetIntAttribute("high"); set => SetIntAttribute("high", value); } /// /// Gets or sets the 'low' HTML5 attribute. /// public int? Low { get => GetIntAttribute("low"); set => SetIntAttribute("low", value); } /// /// Gets or sets the 'max' HTML5 attribute. /// public int? Max { get => GetIntAttribute("max"); set => SetIntAttribute("max", value); } /// /// Gets or sets the 'min' HTML5 attribute. /// public int? Min { get => GetIntAttribute("min"); set => SetIntAttribute("min", value); } /// /// Gets or sets the 'optimum' HTML5 attribute. /// public int? Optimum { get => GetIntAttribute("optimum"); set => SetIntAttribute("optimum", value); } /// /// Gets or sets the 'value' HTML5 attribute. /// public int? Value { get => GetIntAttribute("value"); set => SetIntAttribute("value", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlOlElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Ordered list /// [Obsolete("Use HtmlOlElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class OrderedList : HtmlOlElement { } /// /// The 'ol' HTML5 element. /// /// public class HtmlOlElement : Element { /// /// Initializes a new instance of the class. /// public HtmlOlElement() : base("ol") { } /// /// Gets or sets the 'reversed' HTML5 attribute. /// public bool Reversed { get => HasAttributeLower("reversed"); set => SetFlagAttributeLower("reversed", value); } /// /// Gets or sets the 'start' HTML5 attribute. /// public int? Start { get => GetIntAttribute("start"); set => SetIntAttribute("start", value); } /// /// Gets or sets the 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlOptionElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Option element /// [Obsolete("Use HtmlOptionElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class OptionElement : HtmlOptionElement { } /// /// The 'option' HTML5 element. /// /// public class HtmlOptionElement : Element { /// /// Initializes a new instance of the class. /// public HtmlOptionElement() : base("option") { } internal override void NotifyValue(ElementEventValue entry) { BeginUpdate(); base.NotifyValue(entry); NotifySelected(entry.Checked); EndUpdate(); } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttributeLower("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'label' HTML5 attribute. /// public string? Label { get => GetAttributeLower("label"); set => SetAttributeLower("label", value); } /// /// Gets or sets the 'selected' HTML5 attribute. /// public bool Selected { get => HasAttributeLower("selected"); set => SetFlagAttributeLower("selected", value); } /// /// Gets or sets the 'value' HTML5 attribute. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } internal void NotifyAdded(string parentValue) { if (parentValue == Value) { Selected = true; } } } } ================================================ FILE: src/LaraUI/Elements/HtmlOptionGroupElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.ComponentModel; namespace Integrative.Lara { /// /// Option group /// [Obsolete("Use HtmlOptionGroupElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class OptionGroup : HtmlOptionGroupElement { } /// /// The 'optgroup' HTML5 element. /// /// public class HtmlOptionGroupElement : Element { /// /// Initializes a new instance of the class. /// public HtmlOptionGroupElement() : base("optgroup") { } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttributeLower("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'label' HTML5 attribute. /// public string? Label { get => GetAttributeLower("label"); set => SetAttributeLower("label", value); } /// /// Gets the child options. /// public IEnumerable Options => GetOptions(); private IEnumerable GetOptions() { foreach (var node in Children) { if (node is HtmlOptionElement option) { yield return option; } } } private protected override void OnChildAdded(Node child) { BeginUpdate(); base.OnChildAdded(child); if (ParentElement is HtmlSelectElement parent && child is HtmlOptionElement option && !string.IsNullOrEmpty(parent.Value)) { option.NotifyAdded(parent.Value); } EndUpdate(); } internal void NotifyAdded(string parentValue) { foreach (var child in GetOptions()) { if (child.Value == parentValue) { child.Selected = true; } } } } } ================================================ FILE: src/LaraUI/Elements/HtmlParagraphElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// HTML p element /// public class HtmlParagraphElement : Element { /// /// constructor /// public HtmlParagraphElement() : base("p") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlScriptElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Script element /// [Obsolete("Use HtmlScriptElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Script : HtmlScriptElement { } /// /// The 'script' HTML5 element. /// /// public class HtmlScriptElement : Element { /// /// Initializes a new instance of the class. /// public HtmlScriptElement() : base("script") { } /// /// Gets or sets the 'async' HTML5 attribute. /// public bool Async { get => HasAttributeLower("async"); set => SetFlagAttributeLower("async", value); } /// /// Gets or sets the 'charset' HTML5 attribute. /// public string? Charset { get => GetAttributeLower("charset"); set => SetAttributeLower("charset", value); } /// /// Gets or sets the 'defer' HTML5 attribute. /// public bool Defer { get => HasAttributeLower("defer"); set => SetFlagAttributeLower("defer", value); } /// /// Gets or sets the 'src' HTML5 attribute. /// public string? Src { get => GetAttributeLower("src"); set => SetAttributeLower("src", value); } /// /// Gets or sets the 'type' HTML5 attribute. /// public string? Type { get => GetAttributeLower("type"); set => SetAttributeLower("type", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlSelectElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.ComponentModel; namespace Integrative.Lara { /// /// Select element /// [Obsolete("Use HtmlSelectElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class SelectElement : HtmlSelectElement { } /// /// The 'select' HTML5 element. /// /// public class HtmlSelectElement : Element { /// /// Initializes a new instance of the class. /// public HtmlSelectElement() : base("select") { } internal override void NotifyValue(ElementEventValue entry) { base.NotifyValue(entry); NotifyValue(entry.Value); } /// /// Adds an option. /// /// The option's value. /// The option's text. public HtmlOptionElement AddOption(string value, string text) { var option = new HtmlOptionElement { Value = value }; option.AppendChild(new TextNode(text)); AppendChild(option); return option; } /// /// Gets or sets the 'autofocus' HTML5 attribute. /// public bool Autofocus { get => HasAttributeLower("autofocus"); set => SetFlagAttributeLower("autofocus", value); } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttributeLower("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'multiple' HTML5 attribute. /// public bool Multiple { get => HasAttributeLower("multiple"); set => SetFlagAttributeLower("multiple", value); } /// /// Gets or sets the 'name' HTML5 attribute. /// public string? Name { get => GetAttributeLower("name"); set => SetAttributeLower("name", value); } /// /// Gets or sets the 'required' HTML5 attribute. /// public bool Required { get => HasAttributeLower("required"); set => SetFlagAttributeLower("required", value); } /// /// Gets or sets the 'size' HTML5 attribute. /// public int? Size { get => GetIntAttribute("size"); set => SetIntAttribute("size", value); } /// /// Gets or sets the 'value' HTML5 attribute. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } internal override void AttributeChanged(string attribute, string? value) { BeginUpdate(); base.AttributeChanged(attribute, value); if (attribute == "value") { UpdateChildOptions(value); } EndUpdate(); } private void UpdateChildOptions(string? value) { if (Multiple) { SelectNonExclusiveOption(value); } else { SelectOnlyOption(value); } } private void SelectNonExclusiveOption(string? value) { foreach (var option in GetOptions()) { if (option.Value == value) { option.Selected = true; } } } private void SelectOnlyOption(string? value) { foreach (var option in GetOptions()) { option.Selected = (option.Value == value); } } private protected override void OnChildAdded(Node child) { BeginUpdate(); base.OnChildAdded(child); var value = Value; if (!string.IsNullOrEmpty(value)) { if (child is HtmlOptionElement option) { option.NotifyAdded(value); } else if (child is HtmlOptionGroupElement group) { group.NotifyAdded(value); } } EndUpdate(); } /// /// Gets the child options. /// public IEnumerable Options => GetOptions(); private IEnumerable GetOptions() { foreach (var child in Children) { if (child is HtmlOptionElement option) { yield return option; } else if (child is HtmlOptionGroupElement group) { foreach (var grandchild in group.Options) { yield return grandchild; } } } } } } ================================================ FILE: src/LaraUI/Elements/HtmlSpanElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// HTML span element /// public class HtmlSpanElement : Element { /// /// Constructor /// public HtmlSpanElement() : base("span") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlTableCellElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Table cell element /// [Obsolete("Use HtmlTableCellElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class TableCell : HtmlTableCellElement { } /// /// The 'td' HTML5 element. /// /// public class HtmlTableCellElement : Element { /// /// Initializes a new instance of the class. /// public HtmlTableCellElement() : base("td") { } /// /// Gets or sets the 'colspan' HTML5 attribute. /// public int? ColSpan { get => GetIntAttribute("colspan"); set => SetIntAttribute("colspan", value); } /// /// Gets or sets the 'headers' HTML5 attribute. /// public string? Headers { get => GetAttributeLower("headers"); set => SetAttributeLower("headers", value); } /// /// Gets or sets the 'rowspan' HTML5 attribute. /// public int? RowSpan { get => GetIntAttribute("rowspan"); set => SetIntAttribute("rowspan", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlTableElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Table element /// [Obsolete("Use HtmlTableElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class Table : HtmlTableElement { } /// /// The 'table' HTML5 element. /// /// public class HtmlTableElement : Element { /// /// Initializes a new instance of the class. /// public HtmlTableElement() : base("table") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlTableHeaderElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// Table header element /// [Obsolete("Use HtmlTableHeaderElement instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class TableHeader : HtmlTableHeaderElement { } /// /// The 'th' HTML5 element. /// /// public class HtmlTableHeaderElement : Element { /// /// Initializes a new instance of the class. /// public HtmlTableHeaderElement() : base("th") { } /// /// Gets or sets the 'abbr' HTML5 attribute. /// public string? Abbr { get => GetAttributeLower("abbr"); set => SetAttributeLower("abbr", value); } /// /// Gets or sets the 'colspan' HTML5 attribute. /// public int? ColSpan { get => GetIntAttribute("colspan"); set => SetIntAttribute("colspan", value); } /// /// Gets or sets the 'headers' HTML5 attribute. /// public string? Headers { get => GetAttributeLower("headers"); set => SetAttributeLower("headers", value); } /// /// Gets or sets the 'rowspan' HTML5 attribute. /// public int? RowSpan { get => GetIntAttribute("rowspan"); set => SetIntAttribute("rowspan", value); } /// /// Gets or sets the 'scope' HTML5 attribute. /// public string? Scope { get => GetAttributeLower("scope"); set => SetAttributeLower("scope", value); } /// /// Gets or sets the 'sorted' HTML5 attribute. /// public string? Sorted { get => GetAttributeLower("sorted"); set => SetAttributeLower("sorted", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlTableRowElement.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 2/2021 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// The 'tr' HTML5 element. /// public class HtmlTableRowElement : Element { /// /// Constructor /// public HtmlTableRowElement() : base("tr") { } } } ================================================ FILE: src/LaraUI/Elements/HtmlTableSectionElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System.Collections.Generic; namespace Integrative.Lara { /// /// HTML table section types /// public enum HtmlTableSectionType { /// /// Table body /// Body, /// /// Table header /// Head, /// /// Table footer /// Foot } /// /// HTML table body element /// public class HtmlTableSectionElement : Element { /// /// Associates a table section type with an element tag /// private static readonly Dictionary _SectionTags = new() { { HtmlTableSectionType.Body, "tbody" }, { HtmlTableSectionType.Head, "thead" }, { HtmlTableSectionType.Foot, "tfoot" } }; /// /// constructor /// public HtmlTableSectionElement(HtmlTableSectionType type) : base(_SectionTags[type]) { } } } ================================================ FILE: src/LaraUI/Elements/HtmlTextAreaElement.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; namespace Integrative.Lara { /// /// TextArea element /// [Obsolete("Use HtmlTextArea instead")] [EditorBrowsable(EditorBrowsableState.Never)] public class TextArea : HtmlTextAreaElement { } /// /// The 'textarea' HTML5 element. /// /// public class HtmlTextAreaElement : Element { /// /// Initializes a new instance of the class. /// public HtmlTextAreaElement() : base("textarea") { } internal override void NotifyValue(ElementEventValue entry) { base.NotifyValue(entry); NotifyValue(entry.Value); } /// /// Gets or sets the 'autofocus' HTML5 attribute. /// public bool Autofocus { get => HasAttributeLower("autofocus"); set => SetFlagAttributeLower("autofocus", value); } /// /// Gets or sets the 'cols' HTML5 attribute. /// public int? Cols { get => GetIntAttribute("cols"); set => SetIntAttribute("cols", value); } /// /// Gets or sets the 'dirname' HTML5 attribute. /// public string? Dirname { get => GetAttributeLower("dirname"); set => SetAttributeLower("dirname", value); } /// /// Gets or sets the 'disabled' HTML5 attribute. /// public bool Disabled { get => HasAttributeLower("disabled"); set => SetFlagAttributeLower("disabled", value); } /// /// Gets or sets the 'maxlength' HTML5 attribute. /// public int? MaxLength { get => GetIntAttribute("maxlength"); set => SetIntAttribute("maxlength", value); } /// /// Gets or sets the 'name' HTML5 attribute. /// public string? Name { get => GetAttributeLower("name"); set => SetAttributeLower("name", value); } /// /// Gets or sets the 'placeholder' HTML5 attribute. /// public string? Placeholder { get => GetAttributeLower("placeholder"); set => SetAttributeLower("placeholder", value); } /// /// Gets or sets the 'readonly' HTML5 attribute. /// public bool Readonly { get => HasAttributeLower("readonly"); set => SetFlagAttributeLower("readonly", value); } /// /// Gets or sets the 'readonly' HTML5 attribute. /// public bool Required { get => HasAttributeLower("required"); set => SetFlagAttributeLower("required", value); } /// /// Gets or sets the 'rows' HTML5 attribute. /// public int? Rows { get => GetIntAttribute("rows"); set => SetIntAttribute("rows", value); } /// /// Gets or sets the 'value' property. /// public string? Value { get => GetAttributeLower("value"); set => SetAttributeLower("value", value); } /// /// Gets or sets the 'wrap' HTML5 attribute. /// public string? Wrap { get => GetAttributeLower("wrap"); set => SetAttributeLower("wrap", value); } } } ================================================ FILE: src/LaraUI/Elements/HtmlTitleElement.cs ================================================ /* Copyright (c) 2021 Integrative Software LLC Created: 2/2021 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// HTML5 'title' element /// public class HtmlTitleElement : Element { /// /// Constructor /// public HtmlTitleElement() : base("title") { } } } ================================================ FILE: src/LaraUI/GlobalSuppressions.cs ================================================ // This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "No sync context in ASP.NET Core", Scope = "namespaceanddescendants", Target = "Integrative.Lara")] ================================================ FILE: src/LaraUI/LaraUI.csproj ================================================  netstandard2.1 Integrative.Lara Integrative.Lara Latest Always 0.10.5 Integrative Software LLC Integrative Software LLC Lara Web Engine Lara Web Engine is a lightweight C# framework for web user interface development. Copyright (c) Integrative Software LLC latest enable LICENSE https://github.com/integrativesoft/lara https://github.com/integrativesoft/lara lara, web, html, html5, desktop, gui, cross, framework, mac, osx, platform, ui, blazor, razor 0.10.5 0.10.5 true false true true snupkg True True True True Resources.resx ResXFileCodeGenerator Resources.Designer.cs C:\Users\Pablo\OneDrive\2019\LaraUI\src\LaraUI\Integrative.Lara.xml Version 0.10.4: - Bumped packages Integrative.png ================================================ FILE: src/LaraUI/LaraUI.csproj.DotSettings ================================================  True True True True True True True True True ================================================ FILE: src/LaraUI/Main/Application.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Hosting; using System; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Represents a hosted Lara application /// public sealed class Application : IDisposable { private readonly Published _published; private IModeController? _modeController; /// /// Web host instance created after calling the Start() method /// // ReSharper disable once MemberCanBePrivate.Global public IWebHost? Host { get; private set; } /// /// Defines default error pages /// public ErrorPages ErrorPages { get; } /// /// Constructor /// public Application() { _published = new Published(); ErrorPages = new ErrorPages(_published); ErrorPages.PublishErrorImage(); PublishService(new WebServiceContent { Address = AutocompleteService.Address, ContentType = "application/json", Method = "POST", Factory = () => new AutocompleteService() }); } internal Published GetPublished() => _published; /// /// Removes all published addresses /// public void ClearAllPublished() { _published.ClearAll(); } /// /// Disposable implementation /// public void Dispose() { ClearAllPublished(); _published.Dispose(); Host?.Dispose(); } #region Publishing /// /// Publishes a page with a component /// /// The URL address of the page /// Handler that creates instances of the component public void PublishPage(string address, Func nodeFactory) => _published.Publish(address, new PagePublished(() => new SingleElementPage(nodeFactory))); /// /// Publishes a page. /// /// The URL address of the page /// Handler that creates instances of the page public void PublishPage(string address, Func pageFactory) => _published.Publish(address, new PagePublished(pageFactory)); /// /// Publishes static content. /// /// The URL address of the content. /// The static content to be published. public void PublishFile(string address, StaticContent content) => _published.Publish(address, content); /// /// Publishes a web service /// /// Web service settings public void PublishService(WebServiceContent content) { content = content ?? throw new ArgumentNullException(nameof(content)); _published.Publish(content); } /// /// Publishes a web service /// /// Binary web service settings public void PublishService(BinaryServiceContent content) { content = content ?? throw new ArgumentNullException(nameof(content)); _published.Publish(content); } /// /// Unpublishes an address and its associated content. /// /// The path. public void UnPublish(string path) { _published.UnPublish(path); } /// /// Publishes all classes marked with the attributes [LaraPage] and [LaraWebService] /// public void PublishAssemblies() => AssembliesReader.LoadAssemblies(this); /// /// Unpublished a web service /// /// The URL address of the web service /// The HTTP method of the web service public void UnPublish(string address, string method) { address = address ?? throw new ArgumentNullException(nameof(address)); method = method ?? throw new ArgumentNullException(nameof(method)); _published.UnPublish(address, method); } internal bool TryGetNode(string path, out IPublishedItem item) => _published.TryGetNode(path, out item); internal bool TryGetConnection(Guid guid, out Connection connection) => _published.Connections.TryGetConnection(guid, out connection); internal Connection CreateConnection(IPAddress remoteIp) => GetController().CreateConnection(remoteIp); internal Task ClearEmptyConnection(Connection connection) => _published.Connections.ClearEmptyConnection(connection); private IModeController GetController() { return _modeController ?? throw new MissingMemberException(nameof(Application), nameof(_modeController)); } #endregion #region Web Components /// /// Registers a specific web component /// /// Web component publish options public void PublishComponent(WebComponentOptions options) { options = options ?? throw new ArgumentNullException(nameof(options)); _published.Publish(options); } /// /// Unregisters a specific web component /// /// Tag name to unpublish public void UnPublishWebComponent(string tagName) => _published.UnPublishWebComponent(tagName); internal bool TryGetComponent(string tagName, out Type type) => _published.TryGetComponent(tagName, out type); #endregion #region Server /// /// Starts the web server /// /// Task // ReSharper disable once UnusedMember.Global public Task Start() => Start(new StartServerOptions()); /// /// Starts the web server. Use with 'await'. /// /// The server options. /// Task public async Task Start(StartServerOptions options) { options = options ?? throw new ArgumentNullException(nameof(options)); Host?.Dispose(); CreateModeController(options.Mode); Host = await GetController().Start(this, options); } internal void CreateModeController(ApplicationMode mode) { if (_modeController == null || _modeController.Mode != mode) { _modeController = ModeControllerFactory.Create(this, mode); } } /// /// Stops the web server gracefully /// /// Token to indicate when the stop should not be graceful anymore /// Task public Task Stop(CancellationToken token = default) => GetHost().StopAsync(token); /// /// Returns a task that is completed when the server stops /// /// Token to trigger shutdown /// Task // ReSharper disable once UnusedMember.Global public Task WaitForShutdown(CancellationToken token = default) => Host.WaitForShutdownAsync(token); internal IWebHost GetHost() { return Host ?? throw new MissingMemberException(nameof(Application), nameof(Host)); } #endregion #region Application behavior modes internal double KeepAliveInterval => GetController().KeepAliveInterval; internal bool AllowLocalhostOnly => GetController().LocalhostOnly; internal int DiscardDelay => GetController().DiscardDelay; #endregion #region Testing methods internal void SetHost(IWebHost host) { Host = host; } #endregion } } ================================================ FILE: src/LaraUI/Main/BaseContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal abstract class BaseContext : ILaraContext { public HttpContext Http { get; } public Application Application { get; } internal BaseContext(Application app, HttpContext http) { LaraUI.InternalContext.Value = this; Application = app; Http = http; } } } ================================================ FILE: src/LaraUI/Main/BinaryServiceContent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Binary web service content definitions /// public sealed class BinaryServiceContent { /// /// The URL of the web service (e.g. '/MyService') /// public string Address { get; set; } = string.Empty; /// /// The HTTP method (e.g. 'POST') /// public string Method { get; set; } = "POST"; /// /// The web service's reponse content-type (e.g. 'image/gif') /// public string ContentType { get; set; } = string.Empty; /// /// The method for creating instances of the web service class /// public Func? Factory { get; set; } internal Func GetFactory() { return Factory ?? throw new MissingMemberException(nameof(BinaryServiceContent), nameof(Factory)); } } } ================================================ FILE: src/LaraUI/Main/BinaryServicePublished.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class BinaryServicePublished : IPublishedItem { public Func Factory { get; } private string ContentType { get; } public BinaryServicePublished(BinaryServiceContent content) { Factory = content.GetFactory(); ContentType = content.ContentType; } public async Task Run(Application app, HttpContext http, LaraOptions options) { var context = new WebServiceContext(app, http) { RequestBody = await MiddlewareCommon.ReadBody(http).ConfigureAwait(false) }; var handler = Factory(); var data = Array.Empty(); if (await MiddlewareCommon.RunHandler(http, async () => { data = await handler.Execute(); }).ConfigureAwait(false)) { await SendReply(context, data); } } private async Task SendReply(WebServiceContext context, byte[] data) { WebServicePublished.SendHeader(context, ContentType); await MiddlewareCommon.WriteBuffer(context.Http, data); } } } ================================================ FILE: src/LaraUI/Main/Connection.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; namespace Integrative.Lara { internal sealed class Connection { public Guid Id { get; } public IPAddress RemoteIp { get; } private readonly Dictionary _documents; public SessionStorage Storage { get; } public Session Session { get; } public AsyncEvent Closing { get; } = new AsyncEvent(); public Connection(Guid id, IPAddress remoteId) { Id = id; RemoteIp = remoteId; _documents = new Dictionary(); Storage = new SessionStorage(); Session = new Session(this); } public bool TryGetDocument(Guid virtualId, out Document document) { if (!_documents.TryGetValue(virtualId, out document)) return false; document.UpdateTimestamp(); return true; } public Document CreateDocument(IPage page, double keepAliveInterval) { var virtualId = Connections.CreateCryptographicallySecureGuid(); var document = new Document(page, virtualId, keepAliveInterval); _documents.Add(virtualId, document); return document; } public async Task Discard(Guid documentId) { if (_documents.TryGetValue(documentId, out var document)) { await document.NotifyUnload(); // ReSharper disable once SuspiciousTypeConversion.Global if (document.Page is IDisposable disposable) { disposable.Dispose(); } _documents.Remove(documentId); } } public IEnumerable> GetDocuments() => _documents; public bool IsEmpty => _documents.Count == 0; private bool _closing; public async Task Close() { if (_closing) return; _closing = true; await Closing.InvokeAsync(this, new EventArgs()); Session.Close(); _closing = false; } } } ================================================ FILE: src/LaraUI/Main/Connections.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Security.Cryptography; using System.Threading.Tasks; namespace Integrative.Lara { internal sealed class Connections : IDisposable { private readonly ConcurrentDictionary _connections; private readonly StaleConnectionsCollector _collector; public Connections() : this(StaleConnectionsCollector.DefaultTimerInterval, StaleConnectionsCollector.DefaultExpireInterval) { } public Connections(double cleanupInterval, double expireInterval) { _connections = new ConcurrentDictionary(); _collector = new StaleConnectionsCollector(this, cleanupInterval, expireInterval); } public double StaleCollectionInterval { get => _collector.TimerInterval; set => _collector.TimerInterval = value; } public double StaleExpirationInterval { get => _collector.ExpirationInterval; set => _collector.ExpirationInterval = value; } public Connection CreateConnection(IPAddress remoteIp) { var id = CreateCryptographicallySecureGuid(); var connection = new Connection(id, remoteIp); _connections.TryAdd(id, connection); return connection; } public bool TryGetConnection(Guid id, out Connection connection) { return _connections.TryGetValue(id, out connection); } public async Task Discard(Guid key) { if (_connections.TryGetValue(key, out var connection)) { await connection.Close(); _connections.TryRemove(key, out _); } } public void Clear() { _connections.Clear(); } public static Guid CreateCryptographicallySecureGuid() { using var provider = new RNGCryptoServiceProvider(); var bytes = new byte[16]; provider.GetBytes(bytes); return new Guid(bytes); } public async Task ClearEmptyConnection(Connection connection) { if (connection.IsEmpty) { await Discard(connection.Id); } } public IEnumerable> GetConnections() => _connections; private bool _disposed; public void Dispose() { if (_disposed) return; _disposed = true; _connections.Clear(); _collector.Dispose(); } } } ================================================ FILE: src/LaraUI/Main/GlobalConstants.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { internal static class GlobalConstants { public const string CookieSessionId = "_Lara_SessionId"; public const string GuidFormat = "N"; public const string WindowUnload = "_window_unload"; public const string ServerSideEvent = "_server_event"; public const string MessageKey = "_message"; public const string FilePrefix = "file/"; #if DEBUG public const string LibraryAddress = "/LaraUI-v{0}-debug.js"; #else public const string LibraryAddress = "/LaraUI-v{0}.js"; #endif } } ================================================ FILE: src/LaraUI/Main/IBinaryService.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { /// /// Binary Service handler class /// public interface IBinaryService { /// /// Executes the web service /// /// Response's body Task Execute(); } } ================================================ FILE: src/LaraUI/Main/INavigation.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { /// /// Methods related to document navigation /// public interface INavigation { /// /// Replaces the specified location. /// /// The new URL location. void Replace(string location); /// /// Flushes the partial changes on the document to the client. Useful to report progress. Use with 'await'. /// /// Task Task FlushPartialChanges(); } } ================================================ FILE: src/LaraUI/Main/IPage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { /// /// Interface for web pages /// public interface IPage { /// /// Called when replying to the initial HTTP GET request. /// /// Task Task OnGet(); } } ================================================ FILE: src/LaraUI/Main/IPageContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System; using System.ComponentModel; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Shared interface for both IPageContext and IWebServiceContext /// public interface ILaraContext { /// /// Gets the .NET Core HttpContext instance /// HttpContext Http { get; } /// /// Parent Application object /// Application Application { get; } } /// /// The execution context for events. /// public interface IPageContext : ILaraContext { /// /// Gets the current document. /// Document Document { get; } /// /// Bridge to execute JavaScript on the client /// // ReSharper disable once InconsistentNaming IJsBridge JSBridge { get; } /// /// Methods related to navigation /// INavigation Navigation { get; } /// /// Session tools /// Session Session { get; } } /// /// Bridge to execute JavaScript on the client /// // ReSharper disable once InconsistentNaming public interface IJsBridge { /// /// Submits the specified java script code to execute on the client. The code is executed after the current event finishes on the server and control returns to the client. /// /// The JavaScript code to execute. /// Optional payload to send to the client void Submit(string javaScriptCode, string? payload = null); /// /// Register a custom event that can be called from JavaScript code /// /// Message type identifier /// The handler for the event. [Obsolete("Use instead AddMessageListener() and RemoveMessageListener().")] [EditorBrowsable(EditorBrowsableState.Never)] void OnMessage(string messageId, Func handler); /// /// Register a listener to a custom event that can be called from JavaScript code /// /// Message type identifier /// Handler to execute void AddMessageListener(string messageId, Func handler); /// /// Unregister a listener to a custom event called from JavaScript code /// /// Message type identifier /// Handler to execute void RemoveMessageListener(string messageId, Func handler); /// /// Gets extra event data that can be passed by the client on custom events. /// /// /// The event data. /// string? EventData { get; } /// /// Makes the client start listening for ServerEventFlush() notifications. /// void ServerEventsOn(); /// /// Makes the client stop listening for ServerEventFlush() notifications. /// /// Task Task ServerEventsOff(); } } ================================================ FILE: src/LaraUI/Main/IPublishedItem.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal interface IPublishedItem { Task Run(Application app, HttpContext http, LaraOptions options); } } ================================================ FILE: src/LaraUI/Main/IWebService.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { /// /// Web Service handler class /// public interface IWebService { /// /// Executes the web service /// /// Response's body Task Execute(); } } ================================================ FILE: src/LaraUI/Main/IWebServiceContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Diagnostics.CodeAnalysis; using System.Net; namespace Integrative.Lara { /// /// Context for web service requests /// public interface IWebServiceContext : ILaraContext { /// /// Request's body sent by the client /// string RequestBody { get; } /// /// Status code to return /// HttpStatusCode StatusCode { get; set; } /// /// Gets a Session object when available /// /// Session object /// true when found bool TryGetSession([NotNullWhen(true)] out Session? session); } } ================================================ FILE: src/LaraUI/Main/JSBridge.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.ComponentModel; using System.Threading.Tasks; namespace Integrative.Lara { internal sealed class JsBridge : IJsBridge { private readonly PageContext _parent; public string? EventData { get; internal set; } = string.Empty; public JsBridge(PageContext parent) { _parent = parent; } public void Submit(string javaScriptCode, string? payload = null) { _parent.Document.Enqueue(new SubmitJsDelta { Code = javaScriptCode, Payload = payload }); } [Obsolete("Use instead AddMessageListener() and RemoveMessageListener().")] [EditorBrowsable(EditorBrowsableState.Never)] public void OnMessage(string key, Func handler) { _parent.Document.OnMessage(key, handler); } public void AddMessageListener(string messageId, Func handler) { _parent.Document.AddMessageListener(messageId, handler); } public void RemoveMessageListener(string messageId, Func handler) { _parent.Document.RemoveMessageListener(messageId, handler); } public void ServerEventsOn() => _parent.Document.ServerEventsOn(); public Task ServerEventsOff() => _parent.Document.ServerEventsOff(); } } ================================================ FILE: src/LaraUI/Main/LaraBinaryServiceAttribute.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Marks a class as a web service that replies an array of bytes /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class LaraBinaryServiceAttribute : Attribute { /// /// Web Service's address (e.g. '/myWS') /// public string Address { get; set; } = string.Empty; /// /// Web Service's method (e.g. 'POST') /// public string Method { get; set; } = "POST"; /// /// Web Service's response content-type HTTP header (e.g. 'image/gif') /// public string ContentType { get; set; } = string.Empty; } } ================================================ FILE: src/LaraUI/Main/LaraPageAttribute.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 7/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Declares a class as a web page that gets published with LaraUI.PublishAssemblies() /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class LaraPageAttribute : Attribute { /// /// Page's address (e.g. '/myPage') /// public string Address { get; set; } = string.Empty; /// /// Default constructor /// public LaraPageAttribute() { } /// /// Constuctor with address /// /// Page's address public LaraPageAttribute(string address) : this() { Address = address; } } } ================================================ FILE: src/LaraUI/Main/LaraUI.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Hosting; using System; using System.ComponentModel; using System.Resources; using System.Threading; using System.Threading.Tasks; [assembly: NeutralResourcesLanguage("en-US")] namespace Integrative.Lara { /// /// The main Lara static class /// // ReSharper disable once InconsistentNaming public static class LaraUI { #region Default application (obsolete) private const string PublishObsolete = "Publishing on the default static application has been deprecated, instead create an Application object."; internal static Application DefaultApplication { get; } = new Application(); /// /// Defines default error pages /// [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static ErrorPages ErrorPages => DefaultApplication.ErrorPages; /// /// Removes all published elements /// [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void ClearAll() => DefaultApplication.ClearAllPublished(); /// /// Publishes a page. /// /// The URL address of the page. /// Handler that creates instances of the page [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void Publish(string address, Func pageFactory) => DefaultApplication.PublishPage(address, pageFactory); /// /// Publishes static content. /// /// The URL address of the content. /// The static content to be published. [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void Publish(string address, StaticContent content) => DefaultApplication.PublishFile(address, content); /// /// Publishes a web service /// /// Web service settings [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void Publish(WebServiceContent content) => DefaultApplication.PublishService(content); /// /// Unpublishes an address and its associated content. /// /// The path. [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnPublish(string path) => DefaultApplication.UnPublish(path); /// /// Publishes all classes marked with the attributes [LaraPage] and [LaraWebService] /// [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void PublishAssemblies() => AssembliesReader.LoadAssemblies(DefaultApplication); /// /// Unpublished a web service /// /// The URL address of the web service /// The HTTP method of the web service [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnPublish(string address, string method) => DefaultApplication.UnPublish(address, method); /// /// Registers a specific web component /// /// Web component publush options [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void Publish(WebComponentOptions options) => DefaultApplication.PublishComponent(options); /// /// Unregisters a specific web component /// /// Tag name to unpublish [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnPublishWebComponent(string tagName) => DefaultApplication.UnPublishWebComponent(tagName); #endregion #region Context variables internal static AsyncLocal InternalContext { get; } = new AsyncLocal(); /// /// Returns the context associated with the current task. See also 'Page' and 'Service'. /// public static ILaraContext Context => InternalContext.Value ?? throw new InvalidOperationException(Resources.NoCurrentContext); /// /// Returns the Page context associated the current task, when executing Page events /// public static IPageContext Page => InternalContext.Value as IPageContext ?? throw new InvalidOperationException(Resources.NoCurrentPage); /// /// Returns the WebService context associated with the current task, when executing web services /// public static IWebServiceContext Service => InternalContext.Value as IWebServiceContext ?? throw new InvalidOperationException(Resources.NoCurrentService); /// /// Returns the current document (same as Page.Document) /// public static Document Document => GetContextDocument(Page) ?? throw new InvalidOperationException(Resources.NoCurrentDocument); internal static Document? GetContextDocument(IPageContext? context) { return context?.Document; } #endregion #region Tools /// /// Starts the web server. Use with 'await'. /// /// Task [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static Task StartServer() => StartServer(new StartServerOptions()); /// /// Starts the web server. Use with 'await'. /// /// The server options. /// Task // ReSharper disable once MemberCanBePrivate.Global [Obsolete(PublishObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static async Task StartServer(StartServerOptions options) { await DefaultApplication.Start(options); return DefaultApplication.GetHost(); } /// /// Launches the user's default web browser on the specified address. /// /// The address. public static void LaunchBrowser(string address) => LaraTools.LaunchBrowser(address); /// /// Launches the user's default web browser on the first address of the host passed in parameters. /// /// The host. public static void LaunchBrowser(IWebHost host) => LaraTools.LaunchBrowser(host); /// /// Gets the first URL associated with the given host. /// /// The host. /// string with URL // ReSharper disable once InconsistentNaming public static string GetFirstURL(IWebHost host) { host = host ?? throw new ArgumentNullException(nameof(host)); return LaraTools.GetFirstUrl(host); } /// /// JSON tools /// // ReSharper disable once InconsistentNaming public static LaraJson JSON { get; } = new LaraJson(); /// /// Flushes page modifications to the client on long-running events. Useful to report progress. /// public static Task FlushPartialChanges() { return Page.Navigation.FlushPartialChanges(); } #endregion #region Internal tools /// /// Shorthand for LaraUI.JSON.Parse(LaraUI.Service.RequestBody) /// /// Type for parsing /// Instance of T public static T ParseRequest() where T : class { return JSON.Parse(Service.RequestBody); } #endregion } } ================================================ FILE: src/LaraUI/Main/LaraWebServiceAttribute.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 7/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Declares a class as a web service that gets published with LaraUI.PublishAssemblies() /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class LaraWebServiceAttribute : Attribute { /// /// Web Service's address (e.g. '/myWS') /// public string Address { get; set; } = string.Empty; /// /// Web Service's method (e.g. 'POST') /// public string Method { get; set; } = "POST"; /// /// Web Service's response content-type HTTP header (e.g. 'application/json') /// public string ContentType { get; set; } = "application/json"; } } ================================================ FILE: src/LaraUI/Main/Navigation.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; namespace Integrative.Lara { internal sealed class Navigation : INavigation { private readonly PageContext _context; public string? RedirectLocation { get; private set; } public Navigation(PageContext context) { _context = context; } public void Replace(string location) { if (_context.Http.Request.Method == "GET") { ReplaceGet(location); } else { ReplacePost(location); } } private void ReplaceGet(string location) { RedirectLocation = location; } private void ReplacePost(string location) { ReplaceDelta.Enqueue(_context.Document, location); } public async Task FlushPartialChanges() { if (_context.Socket == null) { throw new InvalidOperationException(Resources.FlushNotAvailable); } if (_context.Document.HasPendingChanges) { await PostEventHandler.FlushPartialChanges(_context.Socket, _context.Document); } } } } ================================================ FILE: src/LaraUI/Main/PageContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Net.WebSockets; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class PageContext : BaseContext, IPageContext { public Document? DocumentInternal { get; internal set; } public Document Document => DocumentInternal ?? throw new MissingMemberException(nameof(PageContext), nameof(Document)); private readonly JsBridge _bridge; private readonly Navigation _navigation; private readonly Connection _connection; public PageContext(Application app, HttpContext http, Connection connection) : base(app, http) { _navigation = new Navigation(this); _bridge = new JsBridge(this); _connection = connection; } public Session Session => _connection.Session; internal WebSocket? Socket { get; set; } public IJsBridge JSBridge => _bridge; public INavigation Navigation => _navigation; public string? RedirectLocation => _navigation.RedirectLocation; internal void SetExtraData(string? data) => _bridge.EventData = data; } } ================================================ FILE: src/LaraUI/Main/PagePublished.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Globalization; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class PagePublished : IPublishedItem { private readonly Func _factory; private HttpStatusCode StatusCode { get; } = HttpStatusCode.OK; public PagePublished(Func factory) { _factory = factory; } public PagePublished(Func factory, HttpStatusCode status) : this(factory) { StatusCode = status; } public async Task Run(Application app, HttpContext http, LaraOptions options) { var connection = GetConnection(app, http); var execution = new PageContext(app, http, connection); var page = CreateInstance(); var document = connection.CreateDocument(page, app.KeepAliveInterval); execution.DocumentInternal = document; if (await RunPage(app, http, page, options).ConfigureAwait(false)) { await ProcessGetResult(http, document, execution, StatusCode); } if (document.CanDiscard) { await connection.Discard(document.VirtualId); } } internal static async Task RunPage(Application app, HttpContext http, IPage page, LaraOptions options) { try { await page.OnGet(); return true; } catch (StatusCodeException status) { await ReplyStatusCodeError(app, http, status, options); return false; } } private static async Task ReplyStatusCodeError(Application app, HttpContext http, StatusCodeException status, LaraOptions options) { if (app.ErrorPages.TryGetPage(status.StatusCode, out var page)) { await page.Run(app, http, options); } else { await MiddlewareCommon.SendStatusReply(http, status.StatusCode, status.Message); } } internal IPage CreateInstance() => _factory(); internal static async Task ProcessGetResult(HttpContext http, Document document, PageContext execution, HttpStatusCode code) { if (!string.IsNullOrEmpty(execution.RedirectLocation)) { http.Response.Redirect(execution.RedirectLocation); } else { document.OpenEventQueue(); var html = WriteDocument(execution.Document); await ReplyDocument(http, html, code); } } internal static Connection GetConnection(Application app, HttpContext http) { return MiddlewareCommon.TryFindConnection(app, http, out var connection) ? connection : CreateConnection(app, http); } private static Connection CreateConnection(Application app, HttpContext http) { var connection = app.CreateConnection(http.Connection.RemoteIpAddress); http.Response.Cookies.Append(GlobalConstants.CookieSessionId, connection.Id.ToString(GlobalConstants.GuidFormat, CultureInfo.InvariantCulture)); return connection; } private static string WriteDocument(Document document) { var writer = new DocumentWriter(document); writer.Print(); return writer.ToString(); } private static async Task ReplyDocument(HttpContext http, string html, HttpStatusCode code) { MiddlewareCommon.SetStatusCode(http, code); MiddlewareCommon.AddHeaderTextHtml(http); MiddlewareCommon.AddHeaderPreventCaching(http); await MiddlewareCommon.WriteUtf8Buffer(http, html); } } } ================================================ FILE: src/LaraUI/Main/Published.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; namespace Integrative.Lara { internal sealed class Published : IDisposable { private readonly Dictionary _published; private readonly ComponentRegistry _components; public Connections Connections { get; } public Published() { _published = new Dictionary(); _components = new ComponentRegistry(); Connections = new Connections(); } public void ClearAll() { _published.Clear(); _components.Clear(); Connections.Clear(); } private bool _disposed; public void Dispose() { if (_disposed) return; _disposed = true; _published.Clear(); Connections.Dispose(); } public void Publish(string path, IPublishedItem item) { _published.Remove(path); _published.Add(path, item); } public void Publish(WebServiceContent content) { var combined = CombineAddress(content.Address, content.Method); _published.Remove(combined); _published.Add(combined, new WebServicePublished(content)); } public void Publish(BinaryServiceContent content) { var combined = CombineAddress(content.Address, content.Method); _published.Remove(combined); _published.Add(combined, new BinaryServicePublished(content)); } private static string CombineAddress(string address, string method) { ValidateAddress(address); ValidateMethod(method); return CombinePathMethod(address, method.ToUpperInvariant()); } public static string CombinePathMethod(string path, string method) { if (method == "GET") { return path; } return path + ">" + method; } internal static void ValidateMethod(string? method) { if (string.IsNullOrEmpty(method)) { throw new ArgumentException(Resources.SpecifyMethodService); } } internal static void ValidateAddress(string? path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException(Resources.SpecifyAddressService); } } public void UnPublish(string path) { _published.Remove(path); } public void UnPublish(string path, string method) { var combined = CombinePathMethod(path, method.ToUpperInvariant()); _published.Remove(combined); } public bool TryGetNode(string path, out IPublishedItem item) { return _published.TryGetValue(path, out item); } public void Publish(WebComponentOptions options) { _components.Register(options.ComponentTagName, options.GetComponentType()); } public void UnPublishWebComponent(string componentTagName) { _components.Unregister(componentTagName); } public bool TryGetComponent(string tagName, out Type type) { return _components.TryGetComponent(tagName, out type); } } } ================================================ FILE: src/LaraUI/Main/Session.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Session information /// public sealed class Session { private readonly Connection _parent; /// /// Occurs when the user closes all browser tabs /// public event EventHandler? Closing; internal event EventHandler? CloseComplete; internal Session(Connection parent) { _parent = parent; } internal void Close() { var args = new EventArgs(); try { Closing?.Invoke(this, args); } // ReSharper disable once EmptyGeneralCatchClause catch { } CloseComplete?.Invoke(this, args); } /// /// Returns an ID that uniquely identifies the UI session /// public Guid SessionId => _parent.Id; /// /// Stores a key value pair /// /// Identifier of the value to store /// Value to store public void SaveValue(string key, string value) => _parent.Storage.Save(key, value); /// /// Removes a stored value /// /// Identifier of the value stored public void RemoveValue(string key) => _parent.Storage.Remove(key); /// /// Retrieves a value stored /// /// Identifier of the value stored /// Value stored /// true if found, false otherwise public bool TryGetValue(string key, out string value) => _parent.Storage.TryGetValue(key, out value); } } ================================================ FILE: src/LaraUI/Main/SessionStorage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Collections.Generic; namespace Integrative.Lara { internal sealed class SessionStorage { private readonly Dictionary _values; private readonly object _mylock; public SessionStorage() { _values = new Dictionary(); _mylock = new object(); } public void Save(string key, string value) { lock (_mylock) { _values.Remove(key); _values.Add(key, value); } } public void Remove(string key) { lock (_mylock) { _values.Remove(key); } } public bool TryGetValue(string key, out string value) { lock (_mylock) { return _values.TryGetValue(key, out value); } } } } ================================================ FILE: src/LaraUI/Main/SingleElementPage.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; namespace Integrative.Lara { internal class SingleElementPage : IPage { private Func ContentFactory { get; } public SingleElementPage(Func contentFactory) { ContentFactory = contentFactory; } public Task OnGet() { var node = ContentFactory(); LaraUI.Document.Body.AppendChild(node); return Task.CompletedTask; } } } ================================================ FILE: src/LaraUI/Main/StaleConnectionsCollector.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Timers; namespace Integrative.Lara { internal sealed class StaleConnectionsCollector : IDisposable { public const double DefaultTimerInterval = 5 * 60 * 1000; // 5 minutes to trigger updates public const double DefaultExpireInterval = 4 * 3600 * 1000; // 4 hours to expire private readonly Connections _connections; private readonly Timer _timer; public double ExpirationInterval { get; set; } public double TimerInterval { get => _timer.Interval; set => _timer.Interval = value; } public StaleConnectionsCollector(Connections connections, double timerInterval = DefaultTimerInterval, double expireInternal = DefaultExpireInterval) { _connections = connections; ExpirationInterval = expireInternal; _timer = new Timer { Interval = timerInterval }; _timer.Elapsed += TimerElapsedHandler; _timer.Start(); } private bool _disposed; public void Dispose() { if (_disposed) return; _disposed = true; _timer.Stop(); _timer.Dispose(); } private async void TimerElapsedHandler(object sender, ElapsedEventArgs e) { if (!_disposed && !_cleaning) { await CleanupExpiredHandler(); } } private bool _cleaning; internal async Task CleanupExpiredHandler() { _cleaning = true; _timer.Enabled = false; await CleanupNonDisposed(); _timer.Enabled = true; _cleaning = false; } private async Task CleanupNonDisposed() { var minRequired = DateTime.UtcNow.AddMilliseconds(-ExpirationInterval); var list = new List>(); foreach (var pair in _connections.GetConnections()) { await CleanupExpired(pair.Value, minRequired); if (pair.Value.IsEmpty) { list.Add(pair); } } foreach (var pair in list) { if (pair.Value.IsEmpty) { await _connections.Discard(pair.Key); } } } internal static async Task CleanupExpired(Connection connection, DateTime minRequired) { var list = new List>(); foreach (var pair in connection.GetDocuments()) { if (pair.Value.LastUtc < minRequired) { list.Add(pair); } } foreach (var pair in list) { if (pair.Value.LastUtc < minRequired) { await connection.Discard(pair.Key); } } } } } ================================================ FILE: src/LaraUI/Main/StaticContent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System; using System.Globalization; using System.Net; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Static content to publish on the web server /// public class StaticContent : IPublishedItem { private const float RequiredCompressionFactor = 0.9f; private readonly byte[] _bytes; /// /// Returns the byte array that is sent to clients /// /// byte array public byte[] GetBytes() => _bytes; /// /// Gets the 'content-type' HTTP header for the static content /// /// /// The 'content-type' value for the content. /// public string ContentType { get; } = string.Empty; /// /// Gets the e-Tag value for the content. /// /// /// The e-Tag value. /// public string ETag { get; } /// /// Gets a value indicating whether this is compressed. /// /// /// true if compressed; otherwise, false. /// public bool Compressed { get; } /// /// Initializes a new instance of the class. /// /// The byte array. /// The 'content-type' HTTP header value. /// The parameter 'bytes' cannot be null. public StaticContent(byte[] bytes, string contentType) : this(bytes) { ContentType = contentType; } /// /// Initializes a new instance of the class. /// /// The byte array. public StaticContent(byte[] bytes) { bytes = bytes ?? throw new ArgumentNullException(nameof(bytes)); var compressed = LaraTools.Compress(bytes); long required = (int)Math.Floor(bytes.LongLength * RequiredCompressionFactor); if (compressed.Length > required) { _bytes = bytes; Compressed = false; } else { _bytes = compressed; Compressed = true; } ETag = ComputeETag(bytes); } /// /// Calculates an ETag value based on the given array of bytes /// /// Array of bytes /// Calculated ETag public static string ComputeETag(byte[] bytes) { var hash = ComputeHash(bytes); return "\"" + hash.ToString(CultureInfo.InvariantCulture) + "\""; } /// /// Formats a hash value in the eTag format /// /// Hash value /// Formatted hash value in eTag format public static string FormatETag(int hash) { return "\"" + hash.ToString(CultureInfo.InvariantCulture) + "\""; } /// /// Computes a hash value for an array of bytes. /// /// Array of bytes /// Calculated hash // ReSharper disable once MemberCanBePrivate.Global public static int ComputeHash(params byte[] data) { data = data ?? throw new ArgumentNullException(nameof(data)); unchecked { const int p = 16777619; var hash = (int)2166136261; // ReSharper disable once LoopCanBeConvertedToQuery foreach (var c in data) hash = (hash ^ c) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; return hash; } } /// /// Public method used by the Lara framework /// /// Lara application /// Http context /// Lara options /// Task public async Task Run(Application app, HttpContext http, LaraOptions options) { http = http ?? throw new ArgumentNullException(nameof(http)); if (IsMatchETag(http.Request.Headers)) { SendMatchStatus(http); } else { await SendContent(http); } } private bool IsMatchETag(IHeaderDictionary headers) { if (!headers.TryGetValue("If-None-Match", out var values)) return false; var eTagClient = values[^1]; return ETag == eTagClient; } private static void SendMatchStatus(HttpContext http) { MiddlewareCommon.SetStatusCode(http, HttpStatusCode.NotModified); } private async Task SendContent(HttpContext http) { var headers = http.Response.Headers; headers.Add("Content-Type", ContentType); headers.Add("Cache-Control", "no-cache"); headers.Add("ETag", ETag); if (Compressed) { headers.Add("Content-Encoding", "deflate"); } await MiddlewareCommon.WriteBuffer(http, _bytes); } } } ================================================ FILE: src/LaraUI/Main/TemplateBuilder.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Globalization; namespace Integrative.Lara { internal static class TemplateBuilder { private static readonly string _LibraryUrl; static TemplateBuilder() { _LibraryUrl = ClientLibraryHandler.GetLibraryPath(); } public static void Build(Document document, double keepAliveInterval) { var head = document.Head; // lang document.Lang = "en"; // UTF-8 var meta = Element.Create("meta"); meta.SetAttribute("charset", "utf-8"); head.AppendChild(meta); // LaraClient.js var script = new HtmlScriptElement { Src = _LibraryUrl, Defer = true }; head.AppendChild(script); // initialization script var tag = Element.Create("script"); var id = document.VirtualId.ToString(GlobalConstants.GuidFormat, CultureInfo.InvariantCulture); var interval = keepAliveInterval.ToString(CultureInfo.InvariantCulture); var code = $"document.addEventListener('DOMContentLoaded', function() {{ LaraUI.initialize('{id}', {interval}); }});"; tag.AppendChild(new TextNode { Data = code }); head.AppendChild(tag); } } } ================================================ FILE: src/LaraUI/Main/WebServiceContent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// Web server content definitions /// public sealed class WebServiceContent { /// /// The URL of the web service (e.g. '/MyService') /// public string Address { get; set; } = string.Empty; /// /// The HTTP method (e.g. 'POST') /// public string Method { get; set; } = "POST"; /// /// The web service's reponse content-type (e.g. 'application/json') /// public string ContentType { get; set; } = "application/json"; /// /// The method for creating instances of the web service class /// public Func? Factory { get; set; } internal Func GetFactory() { return Factory ?? throw new MissingMemberException(nameof(WebServiceContent), nameof(Factory)); } } } ================================================ FILE: src/LaraUI/Main/WebServiceContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class WebServiceContext : BaseContext, IWebServiceContext { public string RequestBody { get; set; } = string.Empty; public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; public WebServiceContext(Application app, HttpContext http) : base(app, http) { } public bool TryGetSession([NotNullWhen(true)] out Session? session) { if (MiddlewareCommon.TryFindConnection(Application, Http, out var connection)) { session = connection.Session; return true; } session = default; return false; } } } ================================================ FILE: src/LaraUI/Main/WebServicePublished.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class WebServicePublished : IPublishedItem { public Func Factory { get; } private string ContentType { get; } public WebServicePublished(WebServiceContent content) { Factory = content.GetFactory(); ContentType = content.ContentType; } public async Task Run(Application app, HttpContext http, LaraOptions options) { var context = new WebServiceContext(app, http) { RequestBody = await MiddlewareCommon.ReadBody(http).ConfigureAwait(false) }; var handler = Factory(); var data = string.Empty; if (await MiddlewareCommon.RunHandler(http, async () => { data = await handler.Execute(); }).ConfigureAwait(false)) { await SendReply(context, data); } } private async Task SendReply(WebServiceContext context, string data) { SendHeader(context, ContentType); await MiddlewareCommon.WriteUtf8Buffer(context.Http, data); } internal static void SendHeader(WebServiceContext context, string contentType) { var http = context.Http; var headers = http.Response.Headers; if (!string.IsNullOrEmpty(contentType)) { headers.Add("Content-Type", contentType); } } } } ================================================ FILE: src/LaraUI/Middleware/BaseHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal abstract class BaseHandler { private readonly RequestDelegate _next; protected BaseHandler(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext http) { try { await TryInvoke(http); } catch (StatusCodeException e) { var text = $"HTTP error {(int)e.StatusCode} '{e.StatusCode}': {e.Message}"; await MiddlewareCommon.SendStatusReply(http, e.StatusCode, text); } } private async Task TryInvoke(HttpContext http) { if (!await ProcessRequest(http).ConfigureAwait(false)) { await _next.Invoke(http); } } internal abstract Task ProcessRequest(HttpContext http); } } ================================================ FILE: src/LaraUI/Middleware/BrowserAppController.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Hosting; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Integrative.Lara { internal class BrowserAppController : BaseModeController { private const double DefaultTimerInterval = 20 * 1000; // 20 seconds to trigger updates private const double DefaultExpireInterval = 60 * 1000; // 60 seconds to expire internal const double BrowserAppKeepAliveInterval = DefaultExpireInterval / 2.5; private Connection? _connection; public override int DiscardDelay => 200; public BrowserAppController(Application app) : base(app, ApplicationMode.BrowserApp) { } public override async Task Start(Application app, StartServerOptions options) { var connections = app.GetPublished().Connections; connections.StaleCollectionInterval = DefaultTimerInterval; connections.StaleExpirationInterval = DefaultExpireInterval; var host = await base.Start(app, options); LaraUI.LaunchBrowser(host); return host; } public override double KeepAliveInterval => BrowserAppKeepAliveInterval; public override Connection CreateConnection(IPAddress remoteIp) { if (!AcceptConnection(remoteIp)) { throw new StatusForbiddenException(Resources.BrowserAppConnectionRejected); } _connection = base.CreateConnection(remoteIp); _connection.Closing.Subscribe(Stop); return _connection; } private Task Stop() { using var source = new CancellationTokenSource(); var token = source.Token; var tasks = new[] { App.Stop(token), SignalStop(source) }; return Task.WhenAll(tasks); } private static Task SignalStop(CancellationTokenSource source) { source.Cancel(); return Task.CompletedTask; } private bool AcceptConnection(IPAddress remoteIp) { return _connection == null && IPAddress.IsLoopback(remoteIp); } public override bool LocalhostOnly => true; } } ================================================ FILE: src/LaraUI/Middleware/ClientEventMessage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Runtime.Serialization; namespace Integrative.Lara { [DataContract] internal sealed class ClientEventMessage { [DataMember] public List? Values { get; set; } [DataMember] public string? ExtraData { get; set; } } } ================================================ FILE: src/LaraUI/Middleware/ClientLibraryHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class ClientLibraryHandler : BaseHandler { private const string ResourceName = "Integrative.Lara.lara-client.js"; private readonly string _address; private readonly byte[] _library; public ClientLibraryHandler(RequestDelegate next) : base(next) { var assembly = GetCurrentAssembly(); var version = GetLibraryVersion(assembly); _address = BuildLibraryAddress(version); var js = LoadLibrary(assembly); _library = Encoding.UTF8.GetBytes(js); } private static Assembly GetCurrentAssembly() { return Assembly.GetAssembly(typeof(ClientLibraryHandler)); } private static string GetLibraryVersion(Assembly assembly) { var info = FileVersionInfo.GetVersionInfo(assembly.Location); var version = info.FileVersion; return version; } private static string BuildLibraryAddress(string version) { version = version.Replace('.', '-'); return GlobalConstants.LibraryAddress.Replace("{0}", version, StringComparison.InvariantCulture); } public static string GetLibraryPath() { var assembly = GetCurrentAssembly(); var version = GetLibraryVersion(assembly); return BuildLibraryAddress(version); } private static string LoadLibrary(Assembly assembly) { using var stream = assembly.GetManifestResourceStream(ResourceName); if (stream == null) throw new InvalidOperationException(Resources.ResourceNotFound); using var reader = new StreamReader(stream); return reader.ReadToEnd(); } public static byte[] LoadFile(Assembly assembly, string name) { using var stream = assembly.GetManifestResourceStream(name); if (stream == null) throw new InvalidOperationException(Resources.ResourceNotFound); var bytes = new byte[stream.Length]; stream.Read(bytes, 0, bytes.Length); return bytes; } internal override async Task ProcessRequest(HttpContext http) { if (http.Request.Method != "GET" || http.Request.Path != _address) return false; await SendLibrary(http); return true; } private async Task SendLibrary(HttpContext http) { #if DEBUG MiddlewareCommon.AddHeaderPreventCaching(http); #else MiddlewareCommon.AddHeaderNeverExpires(http); #endif MiddlewareCommon.AddHeaderJSON(http); await MiddlewareCommon.WriteBuffer(http, _library); } } } ================================================ FILE: src/LaraUI/Middleware/ContentTypes.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { /// /// Constants for 'content-type' HTTP header /// public static class ContentTypes { ///Used to denote the encoding necessary for files containing JavaScript source code. The alternative MIME type for this file type is text/javascript. public const string ApplicationXJavascript = "application/x-javascript"; ///24bit Linear PCM audio at 8-48kHz, 1-N channels; Defined in RFC 3190 public const string AudioL24 = "audio/L24"; ///Adobe Flash files for example with the extension .swf public const string ApplicationXShockwaveFlash = "application/x-shockwave-flash"; ///Arbitrary binary data.[5] Generally speaking this type identifies files that are not associated with a specific application. Contrary to past assumptions by software packages such as Apache this is not a type that should be applied to unknown files. In such a case, a server or application should not indicate a content type, as it may be incorrect, but rather, should omit the type in order to allow the recipient to guess the type.[6] public const string ApplicationOctetStream = "application/octet-stream"; ///Atom feeds public const string ApplicationAtomXml = "application/atom+xml"; ///Cascading Style Sheets; Defined in RFC 2318 public const string TextCss = "text/css"; ///commands; subtype resident in Gecko browsers like Firefox 3.5 public const string TextCmd = "text/cmd"; ///Comma-separated values; Defined in RFC 4180 public const string TextCsv = "text/csv"; ///deb (file format), a software package format used by the Debian project public const string ApplicationXDeb = "application/x-deb"; ///Defined in RFC 1847 public const string MultipartEncrypted = "multipart/encrypted"; ///Defined in RFC 1847 public const string MultipartSigned = "multipart/signed"; ///Defined in RFC 2616 public const string MessageHttp = "message/http"; ///Defined in RFC 4735 public const string ModelExample = "model/example"; ///device-independent document in DVI format public const string ApplicationXDvi = "application/x-dvi"; ///DTD files; Defined by RFC 3023 public const string ApplicationXmlDtd = "application/xml-dtd"; ///ECMAScript/JavaScript; Defined in RFC 4329 public const string ApplicationJavascript = "application/javascript"; ///ECMAScript/JavaScript; Defined in RFC 4329 (equivalent to application/javascript but with stricter processing rules) public const string ApplicationEcmascript = "application/ecmascript"; ///EDI EDIFACT data; Defined in RFC 1767 public const string ApplicationEdifact = "application/EDIFACT"; ///EDI X12 data; Defined in RFC 1767 public const string ApplicationEdiX12 = "application/EDI-X12"; ///Email; Defined in RFC 2045 and RFC 2046 public const string MessagePartial = "message/partial"; ///Email; EML files, MIME files, MHT files, MHTML files; Defined in RFC 2045 and RFC 2046 public const string MessageRfc822 = "message/rfc822"; ///Extensible Markup Language; Defined in RFC 3023 public const string TextXml = "text/xml"; ///Extensible Markup Language; Defined in RFC 3023 public const string ApplicationXml = "application/xml"; ///Flash video (FLV files) public const string VideoXFlv = "video/x-flv"; ///GIF image; Defined in RFC 2045 and RFC 2046 public const string ImageGif = "image/gif"; ///GoogleWebToolkit data public const string TextXGwtRpc = "text/x-gwt-rpc"; ///Gzip public const string ApplicationXGzip = "application/x-gzip"; ///HTML; Defined in RFC 2854 public const string TextHtml = "text/html"; ///HTML; Defined in RFC 2854 public const string TextHtmlUtf8 = "text/html; charset=utf-8"; ///ICO image; Registered[9] public const string ImageVndMicrosoftIcon = "image/vnd.microsoft.icon"; ///IGS files, IGES files; Defined in RFC 2077 public const string ModelIges = "model/iges"; ///IMDN Instant Message Disposition Notification; Defined in RFC 5438 public const string MessageImdnXml = "message/imdn+xml"; ///JavaScript Object Notation JSON; Defined in RFC 4627 public const string ApplicationJson = "application/json"; ///JavaScript Object Notation (JSON) Patch; Defined in RFC 6902 public const string ApplicationJsonPatch = "application/json-patch+json"; ///JPEG JFIF image; Associated with Internet Explorer; Listed in ms775147(v=vs.85) - Progressive JPEG, initiated before global browser support for progressive JPEGs (Microsoft and Firefox). public const string ImagePjpeg = "image/pjpeg"; ///JPEG JFIF image; Defined in RFC 2045 and RFC 2046 public const string ImageJpeg = "image/jpeg"; ///jQuery template data public const string TextXJqueryTmpl = "text/x-jquery-tmpl"; ///KML files (e.g. for Google Earth) public const string ApplicationVndGoogleEarthKmlXml = "application/vnd.google-earth.kml+xml"; ///LaTeX files public const string ApplicationXLatex = "application/x-latex"; ///Matroska open media format public const string VideoXMatroska = "video/x-matroska"; ///Microsoft Excel files public const string ApplicationVndMsExcel = "application/vnd.ms-excel"; ///Microsoft Powerpoint files public const string ApplicationVndMsPowerpoint = "application/vnd.ms-powerpoint"; ///Microsoft Word files[15] public const string ApplicationMsword = "application/msword"; ///MIME Email; Defined in RFC 2045 and RFC 2046 public const string MultipartAlternative = "multipart/alternative"; ///MIME Email; Defined in RFC 2045 and RFC 2046 public const string MultipartMixed = "multipart/mixed"; ///MIME Email; Defined in RFC 2387 and used by MHTML (HTML mail) public const string MultipartRelated = "multipart/related"; ///MIME Webform; Defined in RFC 2388 public const string MultipartFormData = "multipart/form-data"; /// Body contains a URL-encoded query string as per RFC 1867 public const string ApplicationWwwFormUrlEncoded = "application/x-www-form-urlencoded"; ///Mozilla XUL files public const string ApplicationVndMozillaXulXml = "application/vnd.mozilla.xul+xml"; ///MP3 or other MPEG audio; Defined in RFC 3003 public const string AudioMpeg = "audio/mpeg"; ///MP4 audio public const string AudioMp4 = "audio/mp4"; ///MP4 video; Defined in RFC 4337 public const string VideoMp4 = "video/mp4"; ///MPEG-1 video with multiplexed audio; Defined in RFC 2045 and RFC 2046 public const string VideoMpeg = "video/mpeg"; ///MSH files, MESH files; Defined in RFC 2077, SILO files public const string ModelMesh = "model/mesh"; ///mulaw audio at 8 kHz, 1 channel; Defined in RFC 2046 public const string AudioBasic = "audio/basic"; ///Ogg Theora or other video (with audio); Defined in RFC 5334 public const string VideoOgg = "video/ogg"; ///Ogg Vorbis, Speex, Flac and other audio; Defined in RFC 5334 public const string AudioOgg = "audio/ogg"; ///Ogg, a multimedia bitstream container format; Defined in RFC 5334 public const string ApplicationOgg = "application/ogg"; ///OP public const string ApplicationXopXml = "application/xop+xml"; ///p12 files public const string ApplicationXPkcs12 = "application/x-pkcs12"; ///p7b and spc files public const string ApplicationXPkcs7Certificates = "application/x-pkcs7-certificates"; ///p7c files public const string ApplicationXPkcs7Mime = "application/x-pkcs7-mime"; ///p7r files public const string ApplicationXPkcs7Certreqresp = "application/x-pkcs7-certreqresp"; ///p7s files public const string ApplicationXPkcs7Signature = "application/x-pkcs7-signature"; ///Portable Document Format, PDF has been in use for document exchange on the Internet since 1993; Defined in RFC 3778 public const string ApplicationPdf = "application/pdf"; ///Portable Network Graphics; Registered,[8] Defined in RFC 2083 public const string ImagePng = "image/png"; ///PostScript; Defined in RFC 2046 public const string ApplicationPostscript = "application/postscript"; ///QuickTime video; Registered[10] public const string VideoQuicktime = "video/quicktime"; ///RAR archive files public const string ApplicationXRarCompressed = "application/x-rar-compressed"; ///RealAudio; Documented in RealPlayer Customer Support Answer 2559 public const string AudioVndRnRealaudio = "audio/vnd.rn-realaudio"; ///Resource Description Framework; Defined by RFC 3870 public const string ApplicationRdfXml = "application/rdf+xml"; ///RSS feeds public const string ApplicationRssXml = "application/rss+xml"; ///SOAP; Defined by RFC 3902 public const string ApplicationSoapXml = "application/soap+xml"; ///StuffIt archive files public const string ApplicationXStuffit = "application/x-stuffit"; ///SVG vector image; Defined in SVG Tiny 1.2 Specification Appendix M public const string ImageSvgXml = "image/svg+xml"; ///Tag Image File Format (only for Baseline TIFF); Defined in RFC 3302 public const string ImageTiff = "image/tiff"; ///Tarball files public const string ApplicationXTar = "application/x-tar"; ///Textual data; Defined in RFC 2046 and RFC 3676 public const string TextPlain = "text/plain"; ///TrueType Font No registered MIME type, but this is the most commonly used public const string ApplicationXFontTtf = "application/x-font-ttf"; ///vCard (contact information); Defined in RFC 6350 public const string TextVcard = "text/vcard"; ///Vorbis encoded audio; Defined in RFC 5215 public const string AudioVorbis = "audio/vorbis"; ///WAV audio; Defined in RFC 2361 public const string AudioVndWave = "audio/vnd.wave"; ///Web Open Font Format; (candidate recommendation; use application/x-font-woff until standard is official) public const string ApplicationFontWoff = "application/font-woff"; ///WebM Matroska-based open media format public const string VideoWebm = "video/webm"; ///WebM open media format public const string AudioWebm = "audio/webm"; ///Windows Media Audio Redirector; Documented in Microsoft help page public const string AudioXMsWax = "audio/x-ms-wax"; ///Windows Media Audio; Documented in Microsoft KB 288102 public const string AudioXMsWma = "audio/x-ms-wma"; ///Windows Media Video; Documented in Microsoft KB 288102 public const string VideoXMsWmv = "video/x-ms-wmv"; ///WRL files, VRML files; Defined in RFC 2077 public const string ModelVrml = "model/vrml"; ///X3D ISO standard for representing 3D computer graphics, X3D XML files public const string ModelX3DXml = "model/x3d+xml"; ///X3D ISO standard for representing 3D computer graphics, X3DB binary files public const string ModelX3DBinary = "model/x3d+binary"; ///X3D ISO standard for representing 3D computer graphics, X3DV VRML files public const string ModelX3DVrml = "model/x3d+vrml"; ///XHTML; Defined by RFC 3236 public const string ApplicationXhtmlXml = "application/xhtml+xml"; ///ZIP archive files; Registered[7] public const string ApplicationZip = "application/zip"; } } ================================================ FILE: src/LaraUI/Middleware/DefaultErrorPage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara { internal class DefaultErrorPage : IPage { public string Title { get; set; } = string.Empty; public string Message { get; set; } = string.Empty; public Task OnGet() { LoadBootstrap(); ShowContent(); return Task.CompletedTask; } private static void LoadBootstrap() { var head = LaraUI.Page.Document.Head; head.AppendChild(new HtmlLinkElement { Rel = "stylesheet", HRef = "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" }); head.AppendChild(new HtmlScriptElement { Src = "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", Defer = true }); head.AppendChild(new HtmlScriptElement { Src = "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js", Defer = true }); } private void ShowContent() { LaraUI.Document.Body.Child( new HtmlDivElement { Class = "container mt-2"} .Child( new HtmlDivElement { Class = "jumbotron" } .Child( new HtmlImageElement { Src = ServerLauncher.ErrorAddress + ".svg", Height = "100px" }, Document.CreateElement("h1") .Wrap(x => x.Class = "display-4") .Wrap(x => x.InnerText = Title) ), Document.CreateElement("p") .Wrap(x => x.InnerText = Message) ) ); } } } ================================================ FILE: src/LaraUI/Middleware/DiscardHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class DiscardHandler : BaseHandler { private readonly Application _app; public DiscardHandler(Application app, RequestDelegate next) : base(next) { _app = app; } internal override async Task ProcessRequest(HttpContext http) { if (http.Request.Method != "POST" || http.Request.Path != "/_discard" || !DiscardParameters.TryParse(http, out var parameters) || !MiddlewareCommon.TryFindConnection(_app, http, out var connection)) return false; await Task.Delay(_app.DiscardDelay); await connection.Discard(parameters.DocumentId); await _app.ClearEmptyConnection(connection); return true; } } } ================================================ FILE: src/LaraUI/Middleware/DiscardParameters.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class DiscardParameters { public Guid DocumentId { get; private set; } public static bool TryParse(HttpContext context, [NotNullWhen(true)] out DiscardParameters? parameters) { var query = context.Request.Query; return TryParse(query, out parameters); } public static bool TryParse(IQueryCollection query, [NotNullWhen(true)] out DiscardParameters? parameters) { if (MiddlewareCommon.TryGetParameter(query, "doc", out var documentText) && Guid.TryParseExact(documentText, GlobalConstants.GuidFormat, out var documentId)) { parameters = new DiscardParameters { DocumentId = documentId, }; return true; } parameters = default; return false; } } } ================================================ FILE: src/LaraUI/Middleware/ErrorPages.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Net; namespace Integrative.Lara { /// /// This class defines a set of default error pages /// public sealed class ErrorPages { private readonly Dictionary _map; private readonly Dictionary _defaults; private readonly Published _published; internal ErrorPages(Published published) { _published = published; _map = new Dictionary(); _defaults = new Dictionary { { HttpStatusCode.NotFound, new PagePublished(DefaultNotFound, HttpStatusCode.NotFound) }, { HttpStatusCode.InternalServerError, new PagePublished(DefaultServerError, HttpStatusCode.InternalServerError) } }; } /// /// Defines a default page to show for GET requests with a given error code /// /// status code /// handler that creates page public void SetDefaultPage(HttpStatusCode code, Func factory) { _map.Remove(code); var page = new PagePublished(factory); _map.Add(code, page); } /// /// Removes a default error page associated with a status code /// /// status code public void Remove(HttpStatusCode code) { _map.Remove(code); } internal PagePublished GetPage(HttpStatusCode code) { TryGetPage(code, out var page); return page; } internal bool TryGetPage(HttpStatusCode code, out PagePublished page) { return _map.TryGetValue(code, out page) || _defaults.TryGetValue(code, out page); } internal IPage DefaultNotFound() { var url = LaraUI.Context.Http.Request.Path; return new DefaultErrorPage { Title = "Not Found", Message = $"The requested URL '{url}' was not found on this server." }; } internal IPage DefaultServerError() { return new DefaultErrorPage { Title = "Internal Server Error", Message = Resources.ServerErrorMessage }; } internal void PublishErrorPage() { const string address = ServerLauncher.ErrorAddress; var page = new PagePublished(DefaultServerError, HttpStatusCode.InternalServerError); _published.Publish(address, page); var combined = Published.CombinePathMethod(address, "POST"); _published.Publish(combined, page); } internal void PublishErrorImage() { const string address = ServerLauncher.ErrorAddress + ".svg"; var assembly = typeof(LaraUI).Assembly; var bytes = ClientLibraryHandler.LoadFile(assembly, "Integrative.Lara.Assets.Error.svg"); _published.Publish(address, new StaticContent(bytes, "image/svg+xml")); } } } ================================================ FILE: src/LaraUI/Middleware/EventParameters.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace Integrative.Lara { [DataContract] internal class EventParameters { [DataMember] public Guid DocumentId { get; set; } [DataMember] public string ElementId { get; set; } = string.Empty; [DataMember] public string EventName { get; set; } = string.Empty; [DataMember] public long EventNumber { get; set; } [DataMember(IsRequired = false)] public ClientEventMessage? Message { get; set; } public virtual IFormFileCollection? Files { get; set; } public static bool TryParse(IQueryCollection query, [NotNullWhen(true)] out EventParameters? parameters) { if (MiddlewareCommon.TryGetParameter(query, "doc", out var documentText) && MiddlewareCommon.TryGetParameter(query, "el", out var elementId) && MiddlewareCommon.TryGetParameter(query, "ev", out var eventName) && MiddlewareCommon.TryGetParameter(query, "seq", out var sequence) && long.TryParse(sequence, NumberStyles.Any, CultureInfo.InvariantCulture, out var eventNumber) && Guid.TryParseExact(documentText, GlobalConstants.GuidFormat, out var documentId)) { parameters = new EventParameters { DocumentId = documentId, ElementId = elementId, EventName = eventName, EventNumber = eventNumber }; return true; } parameters = default; return false; } public async Task ReadAjaxMessage(HttpContext http) { if (!http.Request.HasFormContentType) { return; } var form = await http.Request.ReadFormAsync(); // TODO: cancellation token for shutdown if (form.TryGetValue(GlobalConstants.MessageKey, out var values)) { Message = LaraTools.Deserialize(values); } Files = form.Files; } } [DataContract] internal class SocketEventParameters : EventParameters { [DataMember(IsRequired = false)] public FormFileCollection? SocketFiles { get; set; } public override IFormFileCollection? Files => SocketFiles; } } ================================================ FILE: src/LaraUI/Middleware/FormFile.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ using System; using System.IO; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { [DataContract] internal class FormFile : IFormFile { [DataMember] public string ContentType { get; set; } = string.Empty; [DataMember] public string ContentDisposition { get; set; } = string.Empty; [DataMember] public string Name { get; set; } = string.Empty; [DataMember] public string FileName { get; set; } = string.Empty; [DataMember] public string Content { get; set; } = string.Empty; private readonly HeaderDictionary _headers = new HeaderDictionary(); public IHeaderDictionary Headers => _headers; [DataMember] public long Length { get; set; } public void CopyTo(Stream target) { var bytes = GetBytes(); target.Write(bytes, 0, bytes.Length); } public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) { var bytes = GetBytes(); return target.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } public Stream OpenReadStream() { var bytes = GetBytes(); return new MemoryStream(bytes); } private byte[] GetBytes() { return Convert.FromBase64String(Content); } } } ================================================ FILE: src/LaraUI/Middleware/FormFileCollection.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ using System; using System.Collections; using System.Collections.Generic; using System.Runtime.Serialization; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { [DataContract] internal class FormFileCollection : IFormFileCollection { [DataMember] public List? InnerList { get; set; } public int Count => GetCount(); private int GetCount() { return InnerList?.Count ?? 0; } public IFormFile this[string name] => GetInnerList().Find(x => x.Name == name); IFormFile IReadOnlyList.this[int index] => GetInnerList()[index]; private List GetInnerList() { return InnerList ?? throw new MissingMemberException(nameof(FormFileCollection), nameof(InnerList)); } public IFormFile GetFile(string name) { return this[name]; } public IReadOnlyList GetFiles(string name) { return GetInnerList().FindAll(x => x.Name == name); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumeratorInternal(); } public IEnumerator GetEnumerator() { return GetEnumeratorInternal(); } private IEnumerator GetEnumeratorInternal() { return InnerList?.GetEnumerator() ?? GetEmptyEnumerator(); } private static IEnumerator GetEmptyEnumerator() { yield break; } } } ================================================ FILE: src/LaraUI/Middleware/IModeController.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Hosting; using System.Net; using System.Threading.Tasks; namespace Integrative.Lara { internal interface IModeController { Task Start(Application app, StartServerOptions options); Connection CreateConnection(IPAddress remoteIp); double KeepAliveInterval { get; } ApplicationMode Mode { get; } bool LocalhostOnly { get; } int DiscardDelay { get; } } internal static class ModeControllerFactory { public static IModeController Create(Application app, ApplicationMode mode) { return mode == ApplicationMode.BrowserApp ? new BrowserAppController(app) : new BaseModeController(app, ApplicationMode.Default); } } internal class BaseModeController : IModeController { internal const double DefaultKeepAliveInterval = StaleConnectionsCollector.DefaultExpireInterval / 2.5; // at least 2 message attempts per expire period protected readonly Application App; public virtual int DiscardDelay => 3000; public BaseModeController(Application app, ApplicationMode mode) { App = app; Mode = mode; } public virtual double KeepAliveInterval => DefaultKeepAliveInterval; public ApplicationMode Mode { get; } public virtual bool LocalhostOnly => false; public virtual Connection CreateConnection(IPAddress remoteIp) { var connections = App.GetPublished().Connections; return connections.CreateConnection(remoteIp); } public virtual Task Start(Application app, StartServerOptions options) { return ServerLauncher.StartServer(app, options); } } } ================================================ FILE: src/LaraUI/Middleware/KeepAliveHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal class KeepAliveHandler : BaseHandler { private static readonly Task _TaskFalse = Task.FromResult(false); private static readonly Task _TaskTrue = Task.FromResult(true); private const string EventPrefix = "/_keepAlive"; private const string AjaxMethod = "POST"; private readonly Application _app; public KeepAliveHandler(Application app, RequestDelegate next) : base(next) { _app = app; } internal override Task ProcessRequest(HttpContext http) { if (!IsMatch(http)) { return _TaskFalse; } TryGetDocument(http, out _); return _TaskTrue; } private static bool IsMatch(HttpContext http) { return http.Request.Path == EventPrefix && !http.WebSockets.IsWebSocketRequest && http.Request.Method == AjaxMethod; } // ReSharper disable once UnusedMethodReturnValue.Local private bool TryGetDocument(HttpContext http, [NotNullWhen(true)] out Document? document) { document = default; return MiddlewareCommon.TryGetParameter(http.Request.Query, "doc", out var text) && Guid.TryParseExact(text, GlobalConstants.GuidFormat, out var documentId) && MiddlewareCommon.TryFindConnection(_app, http, out var connection) && connection.TryGetDocument(documentId, out document); } } } ================================================ FILE: src/LaraUI/Middleware/LaraMiddleware.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Lara middleware class for the ASP.NET Core framework /// public sealed class LaraMiddleware { private readonly RequestDelegate _next; /// /// Initializes a new instance of the class. /// /// The next middleware /// Lara application /// Configuration options public LaraMiddleware(RequestDelegate next, Application app, LaraOptions options) { options = options ?? throw new ArgumentNullException(nameof(options)); next = new ClientLibraryHandler(next).Invoke; next = new PublishedItemHandler(next, app, options).Invoke; next = new DiscardHandler(app, next).Invoke; next = new KeepAliveHandler(app, next).Invoke; _next = new PostEventHandler(app, next).Invoke; } /// /// Invokes this middleware. /// /// The HttpContext. /// Task public async Task Invoke(HttpContext http) { await _next.Invoke(http); } } } ================================================ FILE: src/LaraUI/Middleware/LocalhostFilter.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 4/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; using System.Net; using System.Threading.Tasks; namespace Integrative.Lara { /// /// A middleware class to allow requests from localhost only. /// public sealed class LocalhostFilter { private readonly RequestDelegate _next; private readonly ILogger _logger; /// /// Constructor /// /// Next middleware delegate /// Logger public LocalhostFilter(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } /// /// Invokes this middleware /// /// The HttpContext /// Task public Task Invoke(HttpContext context) { context = context ?? throw new ArgumentNullException(nameof(context)); var remote = context.Connection.RemoteIpAddress; if (IPAddress.IsLoopback(remote)) return _next.Invoke(context); var msg = $"Forbidden request from {remote}"; _logger.LogInformation(msg); return MiddlewareCommon.SendStatusReply(context, HttpStatusCode.Forbidden, Resources.Http403); } } } ================================================ FILE: src/LaraUI/Middleware/MiddlewareCommon.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal static class MiddlewareCommon { public static async Task SendStatusReply(HttpContext context, HttpStatusCode code, string text) { SetStatusCode(context, code); AddHeaderPreventCaching(context); await WriteUtf8Buffer(context, text); } public static async Task WriteUtf8Buffer(HttpContext http, string text) { var buffer = Encoding.UTF8.GetBytes(text); await WriteBuffer(http, buffer); } public static async Task WriteBuffer(HttpContext http, byte[] buffer) { await http.Response.Body.WriteAsync(buffer.AsMemory(0, buffer.Length)); } public static void SetStatusCode(HttpContext http, HttpStatusCode code) { http.Response.StatusCode = (int)code; } public static void AddHeaderPreventCaching(HttpContext context) { context.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); } public static void AddHeaderNeverExpires(HttpContext context) { context.Response.Headers.Add("Cache-Control", "max-age=31556926"); } public static void AddHeaderTextHtml(HttpContext http) { http.Response.Headers.Add("Content-Type", "text/html; charset=utf-8"); } // ReSharper disable once InconsistentNaming public static void AddHeaderJSON(HttpContext http) { http.Response.Headers.Add("Content-Type", "application/json"); } public static bool TryFindConnection(Application app, HttpContext http, [NotNullWhen(true)] out Connection? connection) { connection = null; return http.Request.Cookies.TryGetValue(GlobalConstants.CookieSessionId, out var value) && Guid.TryParseExact(value, GlobalConstants.GuidFormat, out var guid) && app.TryGetConnection(guid, out connection) && connection.RemoteIp.Equals(http.Connection.RemoteIpAddress); } public static bool TryGetParameter(IQueryCollection query, string name, [NotNullWhen(true)] out string? value) { if (query.TryGetValue(name, out var values) && values.Count > 0) { value = values[0]; return true; } value = default; return false; } public static async Task<(bool, T?)> ReadWebSocketMessage(WebSocket socket, int maxSize) where T : class { var buffer = new ArraySegment(new byte[8192]); using var ms = new MemoryStream(); WebSocketReceiveResult result; do { result = await socket.ReceiveAsync(buffer, CancellationToken.None); ms.Write(buffer.Array, buffer.Offset, result.Count); } while (!result.EndOfMessage && result.Count <= maxSize); return ProcessWebSocketMessage(maxSize, ms, result); } internal static (bool, T?) ProcessWebSocketMessage(int maxSize, MemoryStream ms, WebSocketReceiveResult result) where T : class { ms.Seek(0, SeekOrigin.Begin); if (result.MessageType != WebSocketMessageType.Text) { return (false, default); } if (result.Count > maxSize) { return (false, default); } try { var parameters = LaraTools.Deserialize(ms); return (true, parameters); } catch { return (false, default); } } public static async Task ReadBody(HttpContext http) { if (http.Request.Body == null) { return string.Empty; } using var reader = new StreamReader(http.Request.Body, Encoding.UTF8); return await reader.ReadToEndAsync(); } public static async Task RunHandler(HttpContext http, Func handler) { try { await handler(); return true; } catch (StatusCodeException e) { await SendStatusReply(http, e.StatusCode, e.Message); return false; } } } } ================================================ FILE: src/LaraUI/Middleware/NotFoundMiddleware.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System.Net; using System.Threading.Tasks; namespace Integrative.Lara { /// /// A middleware to show a simple 'not found' page /// public class NotFoundMiddleware { private readonly LaraOptions _options; private readonly Application _app; /// /// Creates an instance of NotFoundMiddleware /// /// Next middleware /// Lara application /// Configuration options // ReSharper disable once UnusedParameter.Local public NotFoundMiddleware(RequestDelegate next, Application app, LaraOptions options) { _options = options; _app = app; } /// /// Invokes this middleware /// /// The HttpContext. /// Task public Task Invoke(HttpContext context) { var page = _app.ErrorPages.GetPage(HttpStatusCode.NotFound); return page.Run(_app, context, _options); } } } ================================================ FILE: src/LaraUI/Middleware/PostEventContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Net.WebSockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal class PostEventContext { public Application Application { get; set; } public HttpContext Http { get; set; } public EventParameters? Parameters { get; set; } public WebSocket? Socket { get; set; } public Connection? Connection { get; set; } public Document? Document { get; set; } public Element? Element { get; set; } public PostEventContext(Application app, HttpContext http) { Application = app; Http = http; } public bool SocketRemainsOpen() => Document != null && Parameters != null && Document.SocketRemainsOpen(Parameters.EventName); public bool IsWebSocketRequest => Http.WebSockets.IsWebSocketRequest; public virtual Task> GetSocketCompletion() { var socket = Socket ?? throw new MissingMemberException(nameof(PostEventContext), nameof(Socket)); return GetDocument().GetSocketCompletion(socket); } public Document GetDocument() { return Document ?? throw new MissingMemberException(nameof(PostEventContext), nameof(Document)); } public Connection GetConnection() { return Connection ?? throw new MissingMemberException(nameof(PostEventContext), nameof(Connection)); } public WebSocket GetSocket() { return Socket ?? throw new MissingMemberException(nameof(PostEventContext), nameof(Socket)); } public EventParameters GetParameters() { return Parameters ?? throw new MissingMemberException(nameof(PostEventContext), nameof(EventParameters)); } } } ================================================ FILE: src/LaraUI/Middleware/PostEventHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class PostEventHandler : BaseHandler { public const string EventPrefix = "/_event"; private const string AjaxMethod = "POST"; private const int MaxSizeBytes = 1024000; public static event EventHandler? EventComplete; private static readonly EventArgs _EventArgs = new EventArgs(); private readonly Application _app; public PostEventHandler(Application app, RequestDelegate next) : base(next) { _app = app; } internal override async Task ProcessRequest(HttpContext http) { if (http.Request.Path != EventPrefix) { return false; } if (http.WebSockets.IsWebSocketRequest) { await ProcessWebSocketEvent(_app, http); return true; } if (http.Request.Method != AjaxMethod) return false; await ProcessAjaxRequest(_app, http); return true; } private static async Task ProcessWebSocketEvent(Application app, HttpContext http) { var socket = await http.WebSockets.AcceptWebSocketAsync(); var result = await MiddlewareCommon.ReadWebSocketMessage(socket, MaxSizeBytes); var context = new PostEventContext(app, http) { Http = http, Socket = socket, Parameters = result.Item2 }; if (result.Item1) { await ProcessRequest(context); } else { await context.Socket.CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "Bad request", CancellationToken.None); } } internal static async Task ProcessAjaxRequest(Application app, HttpContext http) { if (EventParameters.TryParse(http.Request.Query, out var parameters)) { await parameters.ReadAjaxMessage(http); var post = new PostEventContext(app, http) { Parameters = parameters }; await ProcessRequest(post); } else { await MiddlewareCommon.SendStatusReply(http, HttpStatusCode.BadRequest, Resources.BadRequest); } } private static async Task ProcessRequest(PostEventContext context) { if (MiddlewareCommon.TryFindConnection(context.Application, context.Http, out var connection) && context.Parameters != null && connection.TryGetDocument(context.Parameters.DocumentId, out var document)) { context.Connection = connection; context.Document = document; await ProcessRequestDocument(context); } else { await SendEvent(context, EventResultType.NoSession); } } internal static async Task ProcessRequestDocument(PostEventContext context) { var document = context.GetDocument(); var parameters = context.GetParameters(); var proceed = await document.WaitForTurn(parameters.EventNumber); if (!proceed) { await SendEvent(context, EventResultType.OutOfSequence); } else if (string.IsNullOrEmpty(parameters.ElementId)) { await ProcessRequestDocument(context, document); } else if (document.TryGetElementById(parameters.ElementId, out var element)) { context.Element = element; await ProcessRequestDocument(context, document); } else { await SendEvent(context, EventResultType.NoElement); } } private static async Task ProcessRequestDocument(PostEventContext context, Document document) { Task release; using (await document.Semaphore.UseWaitAsync()) { release = await RunEvent(context); } await release; } private static async Task RunEvent(PostEventContext post) { var connection = post.GetConnection(); var document = post.GetDocument(); var context = new PageContext(post.Application, post.Http, connection) { Socket = post.Socket, DocumentInternal = document }; ProcessMessageIfNeeded(context, post.Parameters); return await RunEventHandler(post); } internal static async Task RunEventHandler(PostEventContext post) { if (!await MiddlewareCommon.RunHandler(post.Http, () => NotifyEventHandler(post))) return Task.CompletedTask; var document = post.GetDocument(); var queue = document.FlushQueue(); return await SendReply(post, queue); } private static Task NotifyEventHandler(PostEventContext post) { var parameters = post.GetParameters(); if (post.Element != null) return post.Element.NotifyEvent(parameters.EventName); var document = post.GetDocument(); return document.NotifyEvent(parameters.EventName); } internal static void ProcessMessageIfNeeded(PageContext context, EventParameters? parameters) { if (parameters == null) { return; } var message = parameters.Message; if (message != null) { context.SetExtraData(message.ExtraData); if (message.Values != null) { ProcessMessage(context.Document, message); } } var files = parameters.Files; if (files != null) { ProcessFiles(context.Document, files); } } private static void ProcessMessage(Document document, ClientEventMessage message) { document = document ?? throw new ArgumentNullException(nameof(document)); message = message ?? throw new ArgumentNullException(nameof(message)); if (message.Values == null) return; foreach (var row in message.Values) { if (document.TryGetElementById(row.ElementId, out var element)) { element.NotifyValue(row); } } } private static void ProcessFiles(Document document, IFormFileCollection files) { foreach (var file in files) { ProcessFile(document, file); } } private static void ProcessFile(Document document, IFormFile file) { var name = file.Name; if (!TryParsePrefix(name, GlobalConstants.FilePrefix, out var id)) return; if (document.TryGetElementById(id, out var element) && element is HtmlInputElement input) { input.AddFile(file); } } private static bool TryParsePrefix(string name, string prefix, [NotNullWhen(true)] out string? elementId) { if (name.StartsWith(prefix, StringComparison.InvariantCulture)) { elementId = name[prefix.Length..]; return true; } elementId = default; return false; } internal static async Task SendReply(PostEventContext post, string json) { var result = Task.CompletedTask; if (post.IsWebSocketRequest) { if (post.SocketRemainsOpen()) { var completion = await post.GetSocketCompletion(); return completion.Task; } await SendSocketReply(post, json); } else { await SendAjaxReply(post.Http, json); } EventComplete?.Invoke(post.Http, _EventArgs); return result; } private static async Task SendSocketReply(PostEventContext post, string json) { var socket = post.GetSocket(); await FlushMessage(socket, json); await CloseSocket(socket); } public static Task CloseSocket(WebSocket socket) { return socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } public static async Task FlushMessage(WebSocket socket, string json) { var buffer = BuildArraySegment(json); await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); } public static async Task FlushPartialChanges(WebSocket socket, Document document) { var json = document.FlushQueue(); await FlushMessage(socket, json); } internal static ArraySegment BuildArraySegment(string json) { if (string.IsNullOrEmpty(json)) { return new ArraySegment(Array.Empty()); } var bytes = Encoding.UTF8.GetBytes(json); return new ArraySegment(bytes); } private static async Task SendAjaxReply(HttpContext http, string json) { MiddlewareCommon.AddHeaderJSON(http); await MiddlewareCommon.WriteUtf8Buffer(http, json); } private static async Task SendEvent(PostEventContext post, EventResultType type) { var reply = new EventResult { ResultType = type }; var json = reply.ToJSON(); await SendReply(post, json); } } } ================================================ FILE: src/LaraUI/Middleware/PublishedItemHandler.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Integrative.Lara { internal sealed class PublishedItemHandler : BaseHandler { private readonly LaraOptions _options; private readonly Application _app; public PublishedItemHandler(RequestDelegate next, Application app, LaraOptions options) : base(next) { _options = options; _app = app; } internal override async Task ProcessRequest(HttpContext http) { var combined = Published.CombinePathMethod(http.Request.Path, http.Request.Method); if (!_app.TryGetNode(combined, out var item)) return false; await item.Run(_app, http, _options); return true; } } } ================================================ FILE: src/LaraUI/Middleware/Sequencer.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Threading.Tasks; namespace Integrative.Lara { internal class Sequencer { private static readonly Task _TaskProceed = Task.FromResult(true); private static readonly Task _TaskAbort = Task.FromResult(false); private readonly object _lock; private readonly Dictionary> _pending; private long _next; public Sequencer() { _lock = new object(); _pending = new Dictionary>(); _next = 1; } public Task WaitForTurn(long turnNumber) { if (turnNumber == 0) { return _TaskProceed; } TaskCompletionSource? completion; lock (_lock) { if (turnNumber == _next) { _next++; FlushPending(); return _TaskProceed; } if (turnNumber > _next) { completion = new TaskCompletionSource(); _pending.Add(turnNumber, completion); } else { return _TaskAbort; } } return completion.Task; } public void AbortAll() { lock (_lock) { foreach (var item in _pending.Values) { item.SetResult(false); } _pending.Clear(); } } private void FlushPending() { while (_pending.TryGetValue(_next, out var source)) { _pending.Remove(_next); source.SetResult(true); _next++; } } } } ================================================ FILE: src/LaraUI/Middleware/ServerEvent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; namespace Integrative.Lara { /// /// The ServerEvent disposable class represents the life cycle of a server-side event. /// public sealed class ServerEvent : IDisposable { private readonly Document _document; private readonly IDisposable _access; private bool _disposed; internal ServerEvent(Document document) { _document = document; _access = document.Semaphore.UseWait(); } /// /// Flushes partial changes made to the document. /// /// Task public Task FlushPartialChanges() { VerifyNotDisposed(); return _document.ServerEventFlush(); } internal void VerifyNotDisposed() { if (_disposed) { throw new InvalidOperationException(Resources.ServerEventAlreadyDisposed); } } /// /// The dispose method flushes all pending changes in the document. /// public void Dispose() { _disposed = true; _document.ServerEventFlush().Wait(); _access.Dispose(); } } } ================================================ FILE: src/LaraUI/Middleware/ServerEventsController.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Net.WebSockets; using System.Threading.Tasks; namespace Integrative.Lara { internal class ServerEventsController { private readonly Document _document; public ServerEventsController(Document document) { _document = document; } private bool _serverEventsEnabled; private WebSocket? _serverEventsSocket; private TaskCompletionSource? _completion; private bool _flushPending; public ServerEventsStatus ServerEventsStatus => CalculateServerEventsStatus(_serverEventsEnabled, _serverEventsSocket); internal static ServerEventsStatus CalculateServerEventsStatus(bool enabled, WebSocket? socket) { if (!enabled) { return ServerEventsStatus.Disabled; } return socket == null ? ServerEventsStatus.Connecting : ServerEventsStatus.Enabled; } public void ServerEventsOn() { if (_serverEventsEnabled) return; _document.Enqueue(new ServerEventsDelta()); _serverEventsEnabled = true; } public Task ServerEventsOff() { _serverEventsEnabled = false; return DiscardSocket(); } public Task NotifyUnload() => DiscardSocket(); private async Task DiscardSocket() { if (_serverEventsSocket != null) { _completion?.SetResult(true); await PostEventHandler.CloseSocket(_serverEventsSocket); _serverEventsSocket = null; } } public bool SocketRemainsOpen(string eventName) { return _serverEventsEnabled && eventName == GlobalConstants.ServerSideEvent; } public async Task ServerEventFlush() { if (PrepareFlush()) { var json = _document.FlushQueue(); if (_serverEventsSocket != null) { await PostEventHandler.FlushMessage(_serverEventsSocket, json); } } } private bool PrepareFlush() { if (!_serverEventsEnabled) { throw new InvalidOperationException(Resources.ServerEventsNotEnabled); } if (!_document.HasPendingChanges) { return false; } if (_serverEventsSocket != null) return true; _flushPending = true; return false; } public async Task> GetSocketCompletion(WebSocket socket) { await DiscardSocket(); _serverEventsSocket = socket; _completion = new TaskCompletionSource(); await FlushIfPending(); return _completion; } private async Task FlushIfPending() { if (_flushPending) { _flushPending = false; await ServerEventFlush(); } } public ServerEvent StartServerEvent() { return new ServerEvent(_document); } } } ================================================ FILE: src/LaraUI/Middleware/StatusCodeException.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Net; namespace Integrative.Lara { /// /// Exception that returns a specific HTTP status code /// public class StatusCodeException : Exception { /// /// Status code to respond to the client /// public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.InternalServerError; /// /// Constructor /// public StatusCodeException() { } /// /// Constructor /// /// Exception message public StatusCodeException(string message) : base(message) { } /// /// Constructor /// /// Exception message /// Inner exception public StatusCodeException(string message, Exception inner) : base(message, inner) { } /// /// Constructor /// /// Status code public StatusCodeException(HttpStatusCode status) { StatusCode = status; } /// /// Constructor /// /// Status code /// Message public StatusCodeException(HttpStatusCode status, string message) : base(message) { StatusCode = status; } } } ================================================ FILE: src/LaraUI/Middleware/StatusForbiddenException.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Net; namespace Integrative.Lara { /// /// Exception that returns an HTTP status code of Forbidden /// public class StatusForbiddenException : StatusCodeException { /// /// Constructor /// public StatusForbiddenException() : base(HttpStatusCode.Forbidden) { } /// /// Constructor /// /// Exception message public StatusForbiddenException(string message) : base(HttpStatusCode.Forbidden, message) { } /// /// Constructor /// /// Exception message /// Inner exception public StatusForbiddenException(string message, Exception inner) : base(message, inner) { StatusCode = HttpStatusCode.Forbidden; } } } ================================================ FILE: src/LaraUI/NewVersionChecklist.txt ================================================ new version checklist: - edit NuGet documentation with what's new in version - in the LaraClient folder run: npm run release - compile Release C# - upload XML documentation to CDN with Filezilla - pack.bat nuget package - update wiki link to to latest CDN link - upload NuGet package ================================================ FILE: src/LaraUI/Reactive/BindableBase.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.ComponentModel; using System.Runtime.CompilerServices; namespace Integrative.Lara { /// /// Implementation of to simplify models. /// public abstract class BindableBase : INotifyPropertyChanged { /// /// Multicast event for property change notifications. /// public event PropertyChangedEventHandler? PropertyChanged; /// /// Checks if a property already matches a desired value. Sets the property and /// notifies listeners only when necessary. /// /// Type of the property. /// Reference to a property with both getter and setter. /// Desired value for the property. /// /// Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers that /// support CallerMemberName. /// /// /// True if the value was changed, false if the existing value matched the /// desired value. /// protected bool SetProperty(ref T storage, T value, [CallerMemberName] string? propertyName = null) { if (Equals(storage, value)) { return false; } storage = value; OnPropertyChanged(propertyName); return true; } /// /// Notifies listeners that a property value has changed. /// /// /// Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers /// that support . /// protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { if (_holdCounter > 0) { _pendingEvents = true; } else { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } private int _holdCounter; private bool _pendingEvents; /// /// Holds all property changed notifications. /// public void BeginUpdate() { _holdCounter++; } /// /// Stops holding property changed notifications. /// If any property changed pending since BeginUpdate, a single property changed event is triggered. /// public void EndUpdate() { _holdCounter--; if (_holdCounter != 0 || !_pendingEvents) return; _pendingEvents = false; OnPropertyChanged(string.Empty); } } } ================================================ FILE: src/LaraUI/Reactive/BindingExtensions.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System; using System.Collections.ObjectModel; using System.ComponentModel; namespace Integrative.Lara { /// /// Extensions for element binding operations /// public static class BindingExtensions { #region bind properties /// /// Executes code whenever a source object triggers the PropertyChanged event /// /// /// /// /// /// public static TNode Bind( this TNode node, INotifyPropertyChanged source, Action onSourceChange) where TNode : Element { node = node ?? throw new ArgumentNullException(nameof(node)); source = source ?? throw new ArgumentNullException(nameof(source)); node.AddSubscription(source, () => onSourceChange(node)); return node; } /// /// Executes code whenever the element triggers the PropertyChanged event /// /// /// /// /// public static TNode BindBack( this TNode node, Action onChange) where TNode : Element { node = node ?? throw new ArgumentNullException(nameof(node)); node.AddSubscription(node, () => onChange(node)); return node; } #endregion #region bind children /// /// Updates the element's children collection based on an observable collection /// /// /// /// /// /// /// public static TParent BindChildren( this TParent element, ObservableCollection source, Func childFactory) where TParent : Element { element = element ?? throw new ArgumentNullException(nameof(element)); source = source ?? throw new ArgumentNullException(nameof(source)); element.ClearChildren(); foreach (var item in source) { element.AppendChild(childFactory(item)); } element.SubscribeChildren(source, (_, args) => { var updater = new CollectionUpdater(childFactory, element, args); updater.Run(); }); return element; } /// /// Creates element nodes based on a source observable collection /// /// /// /// /// /// /// public static TParent ForEach( this TParent element, ObservableCollection source, Func childFactory) where TParent : Element { element = element ?? throw new ArgumentNullException(nameof(element)); var fragment = new Fragment(); element.AppendChild(fragment); fragment.BindChildren(source, childFactory); return element; } #endregion } } ================================================ FILE: src/LaraUI/Reactive/BindingOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq.Expressions; namespace Integrative.Lara { /// /// Base class for binding options /// public abstract class BindOptions { } /// /// Base class for property-changed based bindings /// public abstract class BindPropertyOptions : BindOptions { } /// /// Base class for property-changed based bindings of source type T /// /// Type of data source public abstract class BindPropertyOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { /// /// Instance to bind to /// public T? BindObject { get; set; } } /// /// Binding options for generic modification handler /// /// Type of data source public sealed class BindHandlerOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { /// /// Action to update the element whenever the data source is modified /// public Action? ModifiedHandler { get; set; } } /// /// Abstract class for text-property bindings /// /// Data type for data source instance /// Data type for data source property public abstract class BindPropertyOptions : BindPropertyOptions where TData : class, INotifyPropertyChanged { /// /// Function to retrieve the target value from instance that's tracked /// public Func? Property { get; set; } } /// /// Binding options for inner text /// /// Type of data source object public sealed class BindInnerTextOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { } /// /// Binding options for attributes /// /// Type of data source object public sealed class BindAttributeOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { /// /// Attribute to bind /// public string Attribute { get; set; } = string.Empty; } /// /// Binding options for flag attributes /// /// Source data type public sealed class BindFlagAttributeOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { /// /// Attribute to bind /// public string Attribute { get; set; } = string.Empty; } /// /// Binding options to toggle element classes /// /// Source data type public sealed class BindToggleClassOptions : BindPropertyOptions where T : class, INotifyPropertyChanged { /// /// Element class to toggle /// public string ClassName { get; set; } = string.Empty; } /// /// Base class for two-way binding /// /// Type of data source /// Type of data property public abstract class BindInputOptions : BindPropertyOptions where TData : class, INotifyPropertyChanged { /// /// Attribute to bind /// public string Attribute { get; set; } = ""; /// /// Bind model property /// public Expression>? Property { get; set; } } /// /// Binding options for two-way binding of attributes /// /// Data source type public sealed class BindInputOptions : BindInputOptions where T : class, INotifyPropertyChanged { } /// /// Binding options for two-way binding of flag attributes /// /// Data source type public sealed class BindFlagInputOptions : BindInputOptions where T : class, INotifyPropertyChanged { } /// /// Binding options for child element collections /// public abstract class BindChildrenOptions : BindOptions { } /// /// Binding options for child element collections /// /// Type of items in observable collection public sealed class BindChildrenOptions : BindChildrenOptions { /// /// Collection that is tracked /// public ObservableCollection Collection { get; } /// /// Method for creating elements /// public Func CreateCallback { get; } /// /// Constructor /// /// Collection to bind /// Method for creating elements public BindChildrenOptions(ObservableCollection collection, Func createCallback) { CreateCallback = createCallback; Collection = collection; } } } ================================================ FILE: src/LaraUI/Reactive/BindingSubscription.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System.ComponentModel; namespace Integrative.Lara { internal class BindingSubscription { public INotifyPropertyChanged Source { get; } public PropertyChangedEventHandler Handler { get; } public BindingSubscription( INotifyPropertyChanged source, PropertyChangedEventHandler handler) { Source = source; Handler = handler; Source.PropertyChanged += handler; } public void Unsubscribe() => Source.PropertyChanged -= Handler; } } ================================================ FILE: src/LaraUI/Reactive/CollectionUpdater.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Collections.Specialized; namespace Integrative.Lara { internal class CollectionUpdater { private readonly Element _element; private readonly NotifyCollectionChangedEventArgs _args; private readonly Func _createCallback; public CollectionUpdater(Func createCallback, Element element, NotifyCollectionChangedEventArgs args) { _createCallback = createCallback; _element = element; _args = args; } public void Run() { switch (_args.Action) { case NotifyCollectionChangedAction.Add: CollectionAdd(); break; case NotifyCollectionChangedAction.Move: CollectionMove(); break; case NotifyCollectionChangedAction.Remove: CollectionRemove(); break; case NotifyCollectionChangedAction.Replace: CollectionReplace(); break; default: CollectionReset(_element); break; } } private void CollectionAdd() { var item = (T)_args.NewItems[0]; var childElement = _createCallback(item); _element.AppendChild(childElement); } private void CollectionMove() { var removeIndex = _args.OldStartingIndex; var addIndex = _args.NewStartingIndex; _element.SwapChildren(addIndex, removeIndex); } private void CollectionRemove() { var index = _args.OldStartingIndex; RemoveAt(index); } private void CollectionReplace() { var value = (T)_args.NewItems[0]; var index = _args.OldStartingIndex; var childElement = _createCallback(value); RemoveAt(index); InsertAt(index, childElement); } private void RemoveAt(int index) { var child = _element.GetChildAt(index); if (child is Element element) { element.UnbindAll(); } _element.RemoveAt(index); } private void InsertAt(int index, Node child) { _element.InsertChildAt(index, child); } private static void CollectionReset(Element element) { UnbindChildren(element); element.ClearChildren(); } private static void UnbindChildren(Element element) { foreach (var node in element.Children) { if (node is Element childElement) { childElement.UnbindAll(); } } } public static void CollectionLoad(BindChildrenOptions options, Element element) { CollectionReset(element); foreach (var item in options.Collection) { var callback = options.CreateCallback; var child = callback(item); element.AppendChild(child); } } } } ================================================ FILE: src/LaraUI/Reactive/ObsoleteElement.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System; using System.ComponentModel; using System.Linq.Expressions; namespace Integrative.Lara { /// /// Obsolete methods on Element class /// public static class ObsoleteElement { internal const string BindObsolete = "Use Bind(source, action) and BindBack(action) instead"; /// /// Binds an element to an action to be triggered whenever the source data changes /// /// Type of the source data /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void Bind(this Element self, BindHandlerOptions options) where T : class, INotifyPropertyChanged { options = options ?? throw new ArgumentNullException(nameof(options)); var handler = options.ModifiedHandler ?? throw new ArgumentNullException(nameof(options.ModifiedHandler)); var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); self.Bind(source, _ => handler(source, self)); } /// /// Binds an attribute /// /// Data type for data source instance /// /// Attribute binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindAttribute(this Element self, BindAttributeOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); var attribute = options.Attribute; self.Bind(source, x => x.SetAttribute(attribute, property(source))); } /// /// Binds a flag attribute /// /// Data type for data source instance /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindFlagAttribute(this Element self, BindFlagAttributeOptions options) where T : class, INotifyPropertyChanged { self.BindToggleAttribute(options); } /// /// Binds a flag attribute /// /// Data type for data source instance /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindToggleAttribute(this Element self, BindFlagAttributeOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var attribute = options.Attribute.ToLowerInvariant(); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); self.Bind(source, x => x.SetFlagAttributeLower(attribute, property(source))); } /// /// Bindings to toggle an element class /// /// Data type for data source instance /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindToggleClass(this Element self, BindToggleClassOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); var className = options.ClassName; if (string.IsNullOrWhiteSpace(className)) throw new ArgumentException("ClassName cannot be empty"); self.Bind(source, x => x.ToggleClass(className, property(source))); } /// /// Two-way bindings for element attributes (e.g. 'value' attribute populated by user) /// /// Source data type /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindInput(this Element self, BindInputOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); if (property.Body is not MemberExpression) { throw new ArgumentException(Resources.InvalidBindingExpression); } var attribute = options.Attribute; if (string.IsNullOrWhiteSpace(attribute)) { throw new ArgumentException("Attribute cannot be empty"); } attribute = attribute.ToLowerInvariant(); var setter = CompileSetter(property); var getter = property.Compile(); self.Bind(source, _ => { var value = getter(source); self.SetAttributeLower(attribute, value); }); self.BindBack(_ => { var value = self.GetAttributeLower(attribute); setter(source, value); }); } /// /// Two-way bindings for element flag attributes (e.g. 'checked' attribute populated by user) /// /// Source data type /// /// Binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindFlagInput(this Element self, BindFlagInputOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); if (property.Body is not MemberExpression) { throw new ArgumentException(Resources.InvalidBindingExpression); } var attribute = options.Attribute; if (string.IsNullOrWhiteSpace(attribute)) { throw new ArgumentException("Attribute cannot be empty"); } attribute = attribute.ToLowerInvariant(); var setter = CompileSetter(property); var getter = property.Compile(); self.Bind(source, _ => { var value = getter(source); self.ToggleAttributeLower(attribute, value); }); self.BindBack(_ => { var value = self.HasAttributeLower(attribute); setter(source, value); }); } internal static Action CompileSetter( Expression> property) { if (property.Body is not MemberExpression member) { throw new ArgumentException(Resources.InvalidBindingExpression); } var param = Expression.Parameter(typeof(TValue), "value"); var set = Expression.Lambda>( Expression.Assign(member, param), property.Parameters[0], param); return set.Compile(); } /// /// Removes bindings for an attribute /// /// /// Attribute to remove bindings of [Obsolete("Has no effect anymore. Use UnbindAll instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnbindAttribute(this Element self, string attribute) { } /// /// Binds an element's inner text /// /// Type of source data /// /// Inner text binding options [Obsolete(BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindInnerText(this Element self, BindInnerTextOptions options) where T : class, INotifyPropertyChanged { var source = options.BindObject ?? throw new ArgumentNullException(nameof(options.BindObject)); var property = options.Property ?? throw new ArgumentNullException(nameof(options.Property)); self.Bind(source, x => x.InnerText = property(source)); } /// /// Removes inner text bindings /// [Obsolete("Has no effect anymore. Use UnbindAll instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnbindInnerText(this Element self) { } /// /// Removes bindings for the generic handler /// [Obsolete("Has no effect anymore. Use UnbindAll instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnbindHandler(this Element self) { } /// /// Removes bindings for any attributes /// [Obsolete("Has no effect anymore. Use UnbindAll instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnbindAttributes(this Element self) { } /// /// Binds the list of children to an observable collection /// /// Type for items in the collection /// /// Children binding options [Obsolete("Use BindChildren(source, factory) instead")] [EditorBrowsable(EditorBrowsableState.Never)] public static void BindChildren(this Element self, BindChildrenOptions options) { self.BindChildren(options.Collection, options.CreateCallback); } /// /// Removes all bindings for the list of children /// [Obsolete("Has no effect anymore, use UnbindAll when needed")] [EditorBrowsable(EditorBrowsableState.Never)] public static void UnbindChildren(this Element self) { } /// /// Clears all child nodes and replaces them with a single text node /// /// /// Text for the node [Obsolete("Use InnerText property instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public static void SetInnerText(this Element self, string text) { self.SetInnerEncode(text, true); } } } ================================================ FILE: src/LaraUI/Resources.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace Integrative.Lara { using System; /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Integrative.Lara.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// /// Looks up a localized string similar to The Provider propery cannot be null.. /// internal static string AutocompleteNullProvider { get { return ResourceManager.GetString("AutocompleteNullProvider", resourceCulture); } } /// /// Looks up a localized string similar to Autocomplete requieres an input element that is already connected to a document.. /// internal static string AutocompleteOrphan { get { return ResourceManager.GetString("AutocompleteOrphan", resourceCulture); } } /// /// Looks up a localized string similar to Bad request. /// internal static string BadRequest { get { return ResourceManager.GetString("BadRequest", resourceCulture); } } /// /// Looks up a localized string similar to Cycle detected: modification handlers should not modify the source data.. /// internal static string BindingCycleDetected { get { return ResourceManager.GetString("BindingCycleDetected", resourceCulture); } } /// /// Looks up a localized string similar to Lara applications running in BrowserApp mode are secured to allow only 1 connection.. /// internal static string BrowserAppConnectionRejected { get { return ResourceManager.GetString("BrowserAppConnectionRejected", resourceCulture); } } /// /// Looks up a localized string similar to Cannot add an element inside itself.. /// internal static string CannotAddInsideItself { get { return ResourceManager.GetString("CannotAddInsideItself", resourceCulture); } } /// /// Looks up a localized string similar to Cannot remove from parent, the node has no parent element.. /// internal static string CannotRemoveNoParent { get { return ResourceManager.GetString("CannotRemoveNoParent", resourceCulture); } } /// /// Looks up a localized string similar to Invalid tag name. It needs to have a '-' and cannot have spaces.. /// internal static string DashRequired { get { return ResourceManager.GetString("DashRequired", resourceCulture); } } /// /// Looks up a localized string similar to The 'EventName' property cannot be null or empty.. /// internal static string EventNameNull { get { return ResourceManager.GetString("EventNameNull", resourceCulture); } } /// /// Looks up a localized string similar to The FlushPartialChanges() method is available only for events declared with the option LongRunnning = true.. /// internal static string FlushNotAvailable { get { return ResourceManager.GetString("FlushNotAvailable", resourceCulture); } } /// /// Looks up a localized string similar to To use the focus() method, first add the element to a document.. /// internal static string FocusDisconnected { get { return ResourceManager.GetString("FocusDisconnected", resourceCulture); } } /// /// Looks up a localized string similar to HTTP 403: Forbidden.. /// internal static string Http403 { get { return ResourceManager.GetString("Http403", resourceCulture); } } /// /// Looks up a localized string similar to Input element cannot be null.. /// internal static string InputElementNull { get { return ResourceManager.GetString("InputElementNull", resourceCulture); } } /// /// Looks up a localized string similar to Invalid expression for binding. Expecting a property setter.. /// internal static string InvalidBindingExpression { get { return ResourceManager.GetString("InvalidBindingExpression", resourceCulture); } } /// /// Looks up a localized string similar to Invalid tag name.. /// internal static string InvalidTagName { get { return ResourceManager.GetString("InvalidTagName", resourceCulture); } } /// /// Looks up a localized string similar to Component types must inherit from the Component class.. /// internal static string MustInherit { get { return ResourceManager.GetString("MustInherit", resourceCulture); } } /// /// Looks up a localized string similar to LaraUI.Context not available. /// internal static string NoCurrentContext { get { return ResourceManager.GetString("NoCurrentContext", resourceCulture); } } /// /// Looks up a localized string similar to There is no current document.. /// internal static string NoCurrentDocument { get { return ResourceManager.GetString("NoCurrentDocument", resourceCulture); } } /// /// Looks up a localized string similar to No page context available.. /// internal static string NoCurrentPage { get { return ResourceManager.GetString("NoCurrentPage", resourceCulture); } } /// /// Looks up a localized string similar to No service context available.. /// internal static string NoCurrentService { get { return ResourceManager.GetString("NoCurrentService", resourceCulture); } } /// /// Looks up a localized string similar to There is no current session.. /// internal static string NoCurrentSession { get { return ResourceManager.GetString("NoCurrentSession", resourceCulture); } } /// /// Looks up a localized string similar to Invalid child/parent nodes specified.. /// internal static string NodeNotFoundInsideParent { get { return ResourceManager.GetString("NodeNotFoundInsideParent", resourceCulture); } } /// /// Looks up a localized string similar to Reference before/after node not found.. /// internal static string ReferenceNodeNotFound { get { return ResourceManager.GetString("ReferenceNodeNotFound", resourceCulture); } } /// /// Looks up a localized string similar to Resource not found. /// internal static string ResourceNotFound { get { return ResourceManager.GetString("ResourceNotFound", resourceCulture); } } /// /// Looks up a localized string similar to The server encountered an internal error or misconfiguration and was unable to complete your request.. /// internal static string ServerErrorMessage { get { return ResourceManager.GetString("ServerErrorMessage", resourceCulture); } } /// /// Looks up a localized string similar to ServerEvent already disposed.. /// internal static string ServerEventAlreadyDisposed { get { return ResourceManager.GetString("ServerEventAlreadyDisposed", resourceCulture); } } /// /// Looks up a localized string similar to Server events are not enabled. Call ServerEventsOn() in order to use ServerEventFlush().. /// internal static string ServerEventsNotEnabled { get { return ResourceManager.GetString("ServerEventsNotEnabled", resourceCulture); } } /// /// Looks up a localized string similar to The 'slot' attribute can only be modified on elements not yet attached to a parent.. /// internal static string SlotOnlyParent { get { return ResourceManager.GetString("SlotOnlyParent", resourceCulture); } } /// /// Looks up a localized string similar to Please specify the address for the web service.. /// internal static string SpecifyAddressService { get { return ResourceManager.GetString("SpecifyAddressService", resourceCulture); } } /// /// Looks up a localized string similar to Please specify the method for the web service (e.g. 'POST').. /// internal static string SpecifyMethodService { get { return ResourceManager.GetString("SpecifyMethodService", resourceCulture); } } /// /// Looks up a localized string similar to Element tag names cannot contain spaces.. /// internal static string TagNameSpaces { get { return ResourceManager.GetString("TagNameSpaces", resourceCulture); } } /// /// Looks up a localized string similar to Too many Pop() calls.. /// internal static string TooManyPops { get { return ResourceManager.GetString("TooManyPops", resourceCulture); } } } } ================================================ FILE: src/LaraUI/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 The Provider propery cannot be null. Autocomplete requieres an input element that is already connected to a document. Bad request Cycle detected: modification handlers should not modify the source data. Lara applications running in BrowserApp mode are secured to allow only 1 connection. Cannot add an element inside itself. Cannot remove from parent, the node has no parent element. Invalid tag name. It needs to have a '-' and cannot have spaces. The 'EventName' property cannot be null or empty. The FlushPartialChanges() method is available only for events declared with the option LongRunnning = true. To use the focus() method, first add the element to a document. HTTP 403: Forbidden. Input element cannot be null. Invalid expression for binding. Expecting a property setter. Invalid tag name. Component types must inherit from the Component class. LaraUI.Context not available There is no current document. No page context available. No service context available. There is no current session. Invalid child/parent nodes specified. Reference before/after node not found. The server encountered an internal error or misconfiguration and was unable to complete your request. ServerEvent already disposed. Server events are not enabled. Call ServerEventsOn() in order to use ServerEventFlush(). The 'slot' attribute can only be modified on elements not yet attached to a parent. Please specify the address for the web service. Please specify the method for the web service (e.g. 'POST'). Element tag names cannot contain spaces. Too many Pop() calls. Resource not found ================================================ FILE: src/LaraUI/Tools/ApplicationBuilderLaraExtensions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Builder; using System; using System.ComponentModel; namespace Integrative.Lara { /// /// ASP.NET Core Extensions for using Lara /// public static class ApplicationBuilderLaraExtensions { /// /// Use the Lara Web Engine. /// /// The application. /// Lara application /// The options. /// app in parameters public static IApplicationBuilder UseLara(this IApplicationBuilder app, Application laraApp, LaraOptions options) { app = app ?? throw new ArgumentNullException(nameof(app)); laraApp = laraApp ?? throw new ArgumentNullException(nameof(laraApp)); options = options ?? throw new ArgumentNullException(nameof(options)); laraApp.CreateModeController(options.Mode); if (options.PublishAssembliesOnStart) { laraApp.PublishAssemblies(); } if (options.AllowLocalhostOnly || laraApp.AllowLocalhostOnly) { app.UseMiddleware(); } if (options.AddWebSocketsMiddleware) { app.UseWebSockets(); } app.UseMiddleware(laraApp, options); if (options.ShowNotFoundPage) { app.UseMiddleware(laraApp, options); } return app; } /// /// Use the Lara Web Engine. /// /// The application. /// The options. /// app in parameters [Obsolete("Specify which Lara Application to use in the parameters of the call")] [EditorBrowsable(EditorBrowsableState.Never)] public static IApplicationBuilder UseLara(this IApplicationBuilder app, LaraOptions options) => UseLara(app, LaraUI.DefaultApplication, options); /// /// Use the Lara Web Engine. /// /// The application. /// app in parameters [Obsolete("Specify which Lara Application to use in the parameters of the call")] [EditorBrowsable(EditorBrowsableState.Never)] public static IApplicationBuilder UseLara(this IApplicationBuilder app) { return UseLara(app, new LaraOptions()); } /// /// Use the Lara Web Engine /// /// ASP.NET Core ApplicationBuilder /// Lara Application /// public static IApplicationBuilder UseLara(this IApplicationBuilder app, Application laraApp) { return UseLara(app, laraApp, new LaraOptions()); } } } ================================================ FILE: src/LaraUI/Tools/AssembliesReader.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 7/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Integrative.Lara { internal static class AssembliesReader { public static void LoadAssemblies(Application app) { foreach (var assembly in GetAssembliesReferencingLara()) { LoadAssembly(app, assembly); } } private static IEnumerable GetAssembliesReferencingLara() { var definedIn = typeof(LaraWebServiceAttribute).Assembly.GetName().Name; foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { if (IncludeAssembly(assembly, definedIn)) { yield return assembly; } } } private static bool IncludeAssembly(Assembly assembly, string definedIn) { return (!assembly.GlobalAssemblyCache) && ((assembly.GetName().Name == definedIn) || assembly.GetReferencedAssemblies().Any(a => a.Name == definedIn)); } private static void LoadAssembly(Application app, Assembly assembly) { try { var types = assembly.GetTypes(); foreach (var type in types) { LoadWebServices(app, type); LoadBinaryServices(app, type); LoadPages(app, type); LoadComponents(app, type); } } catch (ReflectionTypeLoadException) { } } private static void LoadWebServices(Application app, Type type) { var services = type.GetCustomAttributes(typeof(LaraWebServiceAttribute), true); foreach (LaraWebServiceAttribute entry in services) { VerifyType(type, "LaraWebService", typeof(IWebService)); app.PublishService(new WebServiceContent { Address = entry.Address, ContentType = entry.ContentType, Factory = () => (IWebService)Activator.CreateInstance(type), Method = entry.Method }); } } internal static void VerifyType(Type assemblyType, string attribute, Type requiredType) { if (requiredType.IsAssignableFrom(assemblyType)) return; var message = $"The class {assemblyType.FullName} marked as [{attribute}] needs to implement/inherit [{requiredType.Name}]."; throw new InvalidOperationException(message); } private static void LoadBinaryServices(Application app, Type type) { var services = type.GetCustomAttributes(typeof(LaraBinaryServiceAttribute), true); foreach (LaraBinaryServiceAttribute entry in services) { VerifyType(type, "LaraBinaryService", typeof(IBinaryService)); app.PublishService(new BinaryServiceContent { Address = entry.Address, ContentType = entry.ContentType, Factory = () => (IBinaryService)Activator.CreateInstance(type), Method = entry.Method }); } } private static void LoadPages(Application app, Type type) { var pages = type.GetCustomAttributes(typeof(LaraPageAttribute), true); foreach (LaraPageAttribute entry in pages) { VerifyType(type, "LaraPage", typeof(IPage)); app.PublishPage(entry.Address, () => (IPage)Activator.CreateInstance(type)); } } private static void LoadComponents(Application app, Type type) { var components = type.GetCustomAttributes(typeof(LaraWebComponentAttribute), true); foreach (LaraWebComponentAttribute entry in components) { VerifyType(type, "LaraWebComponent", typeof(WebComponent)); var tagName = string.IsNullOrEmpty(entry.ComponentTagName) ? Element.GetDefaultTagName(type) : entry.ComponentTagName; app.PublishComponent(new WebComponentOptions { ComponentTagName = tagName, ComponentType = type }); } } } } ================================================ FILE: src/LaraUI/Tools/AsyncEvent.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Delegate for asynchronous events /// /// Type of event arguments /// Sender of event /// Event arguments /// Task public delegate Task AsyncEventHandler(object sender, T args) where T : EventArgs; /// /// Asynchronous event source /// public class AsyncEvent : AsyncEvent { } /// /// Generic asynchronous event source /// /// Type of event arguments public class AsyncEvent where T : EventArgs { private readonly HashSet> _handlers; private readonly HashSet> _batons; private readonly HashSet> _actions; private readonly HashSet _voids; /// /// Constructor /// public AsyncEvent() { _handlers = new HashSet>(); _batons = new HashSet>(); _actions = new HashSet>(); _voids = new HashSet(); } /// /// Adds an action to execute when this event is fired /// /// action to execute public void Subscribe(Func action) { action = action ?? throw new ArgumentNullException(nameof(action)); _actions.Add(action); } /// /// Adds a method to execute when the event is fired /// /// delegate public void Subscribe(AsyncEventHandler handler) { handler = handler ?? throw new ArgumentNullException(nameof(handler)); _handlers.Add(handler); } /// /// Adds an event to trigger when this event is fired /// /// event to trigger public void Subscribe(AsyncEvent other) { other = other ?? throw new ArgumentNullException(nameof(other)); _batons.Add(other); } /// /// Subscribes an action to execute when the event is triggered /// /// Action public void Subscribe(Action handler) { handler = handler ?? throw new ArgumentNullException(nameof(handler)); _voids.Add(handler); } /// /// Unsubscribes a previously subscribed handler /// /// handler public void Unsubscribe(AsyncEventHandler handler) { handler = handler ?? throw new ArgumentNullException(nameof(handler)); _handlers.Remove(handler); } /// /// Unsubscribes a previously subscribed handler /// /// handler public void Unsubscribe(AsyncEvent handler) { handler = handler ?? throw new ArgumentNullException(nameof(handler)); _batons.Remove(handler); } /// /// Unsubscribes a previously subscribed action /// /// Action public void Unsubscribe(Func action) { action = action ?? throw new ArgumentNullException(nameof(action)); _actions.Remove(action); } /// /// Unsubscribes a previously subscribed action /// /// Action public void Unsubscribe(Action action) { action = action ?? throw new ArgumentNullException(nameof(action)); _voids.Remove(action); } /// /// Invokes an event /// /// Event's source /// Event's arguments /// Task public async Task InvokeAsync(object sender, T args) { foreach (var handler in _handlers) { await handler(sender, args); } foreach (var baton in _batons) { await baton.InvokeAsync(sender, args); } foreach (var action in _actions) { await action(); } foreach (var method in _voids) { method(); } } } } ================================================ FILE: src/LaraUI/Tools/ClassEditor.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ namespace Integrative.Lara { internal static class ClassEditor { public static string? AddClass(string? previous, string name) { if (HasClass(previous, name)) { return previous; } if (string.IsNullOrWhiteSpace(previous)) { return name; } return previous.TrimEnd() + " " + name; } public static string? RemoveClass(string? previous, string name) { if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(previous)) { return previous; } name = name.Trim(); previous = previous.Trim(); if (previous == name) { return ""; } if (previous.StartsWith(name + " ", System.StringComparison.InvariantCulture)) { return previous.Substring(name.Length + 1); } if (previous.EndsWith(" " + name, System.StringComparison.InvariantCulture)) { return previous.Substring(0, previous.Length - name.Length - 1); } var index = previous.IndexOf(" " + name + " ", System.StringComparison.InvariantCulture); if (index == -1) { return previous; } var left = previous.Substring(0, index); var right = previous.Substring(index + name.Length + 1); return left + right; } public static string? ToggleClass(string? previous, string name, bool value) { return value ? AddClass(previous, name) : RemoveClass(previous, name); } public static string? ToggleClass(string? previous, string name) { var value = HasClass(previous, name); return ToggleClass(previous, name, !value); } public static bool HasClass(string? elementClass, string className) { if (string.IsNullOrWhiteSpace(className)) { return true; } if (string.IsNullOrWhiteSpace(elementClass)) { return false; } elementClass = elementClass.Trim(); return elementClass == className || elementClass.StartsWith(className + " ", System.StringComparison.InvariantCulture) || elementClass.EndsWith(" " + className, System.StringComparison.InvariantCulture) || elementClass.Contains(" " + className + " ", System.StringComparison.InvariantCulture); } } } ================================================ FILE: src/LaraUI/Tools/DocumentLocal.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; namespace Integrative.Lara { /// /// Represents ambient data that is local to the current document. /// /// Value public class DocumentLocal { private readonly Dictionary _storage; /// /// Constructor /// public DocumentLocal() { _storage = new Dictionary(); } /// /// Value /// public T Value { get => GetValue(); set => SetValue(value); } private T GetValue() { var document = GetDocument(); _storage.TryGetValue(document, out var value); return value; } private void SetValue(T value) { var document = GetDocument(); if (_storage.TryGetValue(document, out var previous)) { if (LaraTools.SameValue(previous, value)) { return; } _storage.Remove(document); _storage.Add(document, value); } else { _storage.Add(document, value); document.UnloadComplete += (_, _) => _storage.Remove(document); } } private static Document GetDocument() { if (LaraUI.Page == null) { throw new InvalidOperationException(Resources.NoCurrentDocument); } return LaraUI.Page.Document; } } } ================================================ FILE: src/LaraUI/Tools/LaraBuilder.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq.Expressions; using System.Threading.Tasks; namespace Integrative.Lara { /// /// Class to build pages more easily /// public sealed class LaraBuilder { #region Constructor private readonly Stack _stack; /// /// Initializes a new instance of the class. /// /// The starting element to build on. public LaraBuilder(Element startingElement) { startingElement = startingElement ?? throw new ArgumentNullException(nameof(startingElement)); _stack = new Stack(); _stack.Push(startingElement); } #endregion #region Pushing and popping elements /// /// Adds a new element and positions the builder into it. /// /// Element's tag name. /// Element's class name /// element's identifier. /// This instance public LaraBuilder Push(string tagName, string? className = null, string? id = null) { return Push(tagName, className, id, out _); } /// /// Adds a new element and positions the builder into it. /// /// Element's tag name. /// Element's class name /// The identifier. /// The element added. /// This instance public LaraBuilder Push(string tagName, string? className, string? id, out Element added) { added = Element.Create(tagName); if (!string.IsNullOrEmpty(id)) { added.Id = id; } if (!string.IsNullOrEmpty(className)) { added.Class = className; } return Push(added); } /// /// Adds a new namespace-specific element (e.g. 'svg') and positions the builder into it. /// /// The namespace of the element. /// The element's tag name /// This instance // ReSharper disable once InconsistentNaming public LaraBuilder PushNS(string ns, string tagName) { var added = Element.CreateNS(ns, tagName); return Push(added); } /// /// Adds an element and positions the builder into it. /// /// The element. /// This instance public LaraBuilder Push(Element element) { element = element ?? throw new ArgumentNullException(nameof(element)); _stack.Push(element); return this; } /// /// Adds an element and positions the builder into it. /// /// The element to add. /// Name of the class. /// This instance public LaraBuilder Push(Element element, string? className) { className = className ?? throw new ArgumentNullException(nameof(className)); Push(element); element.Class = className; return this; } /// /// Positions the builder one position back on the element stack. /// /// This instance /// Too many Pop() calls. public LaraBuilder Pop() { if (_stack.Count <= 1) { throw new InvalidOperationException(Resources.TooManyPops); } var pop = _stack.Pop(); if (_stack.Count <= 0) return this; var current = _stack.Peek(); current.AppendChild(pop); return this; } /// /// Returns the current element in the out variable /// /// Current element /// This instance public LaraBuilder GetCurrent(out Element element) { element = _stack.Peek(); return this; } #endregion #region Adding nodes /// /// Adds a text node. /// /// The text. /// if set to true [encode]. /// This instance [Obsolete("Use AppendText() or AppendData() instead of AddTextNode")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder AddTextNode(string text, bool encode = true) { return AddTextNode(new TextNode(text, encode)); } /// /// Adds a text node. /// /// The node. /// This instance [Obsolete("Use AppendText() or AppendData() instead of AddTextNode")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder AddTextNode(TextNode node) { node = node ?? throw new ArgumentNullException(nameof(node)); return AddNode(node); } /// /// Appends text to the current element /// /// Text to append /// This instance public LaraBuilder AppendText(string text) { return AppendEncode(text, true); } /// /// Appends raw HTML to the current element /// /// raw HTML /// This instance public LaraBuilder AppendData(string data) { return AppendEncode(data, false); } private LaraBuilder AppendEncode(string value, bool encode) { if (!string.IsNullOrEmpty(value)) { _stack.Peek().AppendEncode(value, encode); } return this; } /// /// Clears all child elements and replaces them with a text node /// /// Text /// This instance public LaraBuilder InnerText(string text) { return InnerEncode(text, true); } /// /// Clears all child elements and replaces them with a text node /// /// raw HTML code /// This instance public LaraBuilder InnerData(string data) { return InnerEncode(data, false); } private LaraBuilder InnerEncode(string data, bool encode) { if (!string.IsNullOrEmpty(data)) { _stack.Peek().SetInnerEncode(data, encode); } return this; } /// /// Adds a node. /// /// The node. /// This instance public LaraBuilder AddNode(Node node) { node = node ?? throw new ArgumentNullException(nameof(node)); var current = _stack.Peek(); current.AppendChild(node); return this; } /// /// Adds a collection of nodes. /// /// The nodes. /// This instance public LaraBuilder AddNodes(IEnumerable nodes) { nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); var current = _stack.Peek(); foreach (var node in nodes) { current.AppendChild(node); } return this; } /// /// Adds a collection of elements. /// /// The nodes. /// This instance public LaraBuilder AddNodes(IEnumerable nodes) { nodes = nodes ?? throw new ArgumentNullException(nameof(nodes)); var current = _stack.Peek(); foreach (var node in nodes) { current.AppendChild(node); } return this; } /// /// Adds the elements generated by the executing the specified handler. /// /// The action. /// This instance public LaraBuilder Add(Action action) { action = action ?? throw new ArgumentNullException(nameof(action)); action(this); return this; } #endregion #region Current element attributes /// /// Sets an attribute on the current element. /// /// The attribute. /// The value. /// Thsi instance public LaraBuilder Attribute(string attribute, string value) { var current = _stack.Peek(); current.SetAttribute(attribute, value); return this; } /// /// Creates an ID for the current element if it doesn't have one /// /// This instance [Obsolete("Not needed anymore")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder EnsureElementId() { return this; } /// /// Sets a flag attribute value. /// /// The attribute. /// if set to true [value]. /// This instance public LaraBuilder FlagAttribute(string attribute, bool value) { var current = _stack.Peek(); current.SetFlagAttribute(attribute, value); return this; } /// /// Adds a class to the current element /// /// class to add /// This instance public LaraBuilder AddClass(string className) { _stack.Peek().AddClass(className); return this; } /// /// Removes a class from the current element /// /// Class to remove /// This instance public LaraBuilder RemoveClass(string className) { _stack.Peek().RemoveClass(className); return this; } /// /// Adds or removes a class from the current element /// /// Class to toggle /// This instamce public LaraBuilder ToggleClass(string className) { _stack.Peek().ToggleClass(className); return this; } /// /// Adds or removes a class from the current element /// /// Class to toggle /// true to add, false to remove /// This instance public LaraBuilder ToggleClass(string className, bool value) { _stack.Peek().ToggleClass(className, value); return this; } #endregion #region Current element events /// /// Associates an event handler with the current element. /// /// The settings. /// This instance public LaraBuilder On(EventSettings settings) { _stack.Peek().On(settings); return this; } /// /// Associates an event handler with the current element. /// /// Name of the event. /// The handler. /// This instance public LaraBuilder On(string eventName, Func handler) { _stack.Peek().On(eventName, handler); return this; } /// /// Associates an event handler with the current element. /// /// Name of the event /// Action to execute /// This instance public LaraBuilder On(string eventName, Action handler) { _stack.Peek().On(eventName, () => { handler(); return Task.CompletedTask; }); return this; } /// /// Listens to an event, only purpose to flush data from the client to the server. /// This forces the server to read input values on the client on the given event name. /// /// event to listen /// This instance public LaraBuilder FlushEvent(string eventName) { return On(eventName, () => Task.CompletedTask); } #endregion #region Bindings /// /// Executes code whenever a source object triggers the PropertyChanged event /// /// /// /// public LaraBuilder Bind(INotifyPropertyChanged source, Action onSourceChange) { BindingExtensions.Bind(_stack.Peek(), source, onSourceChange); return this; } /// /// Executes code whenever the element triggers the PropertyChanged event /// /// /// public LaraBuilder BindBack(Action onChange) { BindingExtensions.BindBack(_stack.Peek(), onChange); return this; } /// /// Updates the element's children collection based on an observable collection /// /// /// /// /// public LaraBuilder BindChildren(ObservableCollection source, Func childFactory) { BindingExtensions.BindChildren(_stack.Peek(), source, childFactory); return this; } #endregion #region Current element bindings /// /// Adds bindings for an attribute /// /// Data type for data source /// Attribute /// Data source instance /// Data source's property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindAttribute(new BindAttributeOptions { Attribute = attribute, BindObject = instance, Property = _ => property() }); } /// /// Adds bindings for a flag attribute /// /// Data type for data source /// Attribute /// Data source instance /// Data source property /// This instance [Obsolete("Use BindToggleAttribute() instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindFlagAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleAttribute(attribute, instance, property); } /// /// Adds bindings for a flag attribute /// /// Data type for data source /// Attribute /// Data source instance /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleAttribute(new BindFlagAttributeOptions { Attribute = attribute, BindObject = instance, Property = _ => property() }); } /// /// Adds bindings for toggling an element class /// /// Data type for the data source /// Class name /// Data source instance /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleClass(string className, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleClass(new BindToggleClassOptions { ClassName = className, BindObject = instance, Property = _ => property() }); } /// /// Adds bindings for an attribute /// /// Data type for data source instance /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindAttribute(BindAttributeOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindAttribute(options); return this; } /// /// Adds bindings for a flag attribute /// /// Data type for data source instance /// Binding options /// This instance [Obsolete("Use BindToggleAttribute() instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindFlagAttribute(BindFlagAttributeOptions options) where T : class, INotifyPropertyChanged { return BindToggleAttribute(options); } /// /// Adds bindings for a flag attribute /// /// Data type for data source instance /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleAttribute(BindFlagAttributeOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindToggleAttribute(options); return this; } /// /// Adds bindings for toggling classes /// /// Data type for data source instance /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleClass(BindToggleClassOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindToggleClass(options); return this; } /// /// Adds bindings for an attribute /// /// Data type for data source instance /// Attribute /// Data source instance /// Data source's property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindAttribute(new BindAttributeOptions { Attribute = attribute, BindObject = instance, Property = property }); } /// /// Adds bindings for a flag attribute /// /// Data type for data source instance /// Attribute /// Data source instance /// Data source property /// This instance [Obsolete("Use instad the BindToggleAttribute() method.")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindFlagAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleAttribute(attribute, instance, property); } /// /// Adds bindings for a flag attribute /// /// Data type for data source instance /// Attribute /// Data source instance /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleAttribute(string attribute, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleAttribute(new BindFlagAttributeOptions { Attribute = attribute, BindObject = instance, Property = property }); } /// /// Adds bindings for toggling a class /// /// Data type for data source instance /// Class name /// Data source instance /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindToggleClass(string className, T instance, Func property) where T : class, INotifyPropertyChanged { return BindToggleClass(new BindToggleClassOptions { ClassName = className, BindObject = instance, Property = property }); } /// /// Adds bindings for inner text /// /// Type of data source /// Data source /// Data source's property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindInnerText(T instance, Func property) where T : class, INotifyPropertyChanged { return BindInnerText(new BindInnerTextOptions { BindObject = instance, Property = _ => property() }); } /// /// Adds bindings for inner text /// /// Type of data source /// Data source /// Data source's property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindInnerText(T instance, Func property) where T : class, INotifyPropertyChanged { return BindInnerText(new BindInnerTextOptions { BindObject = instance, Property = property }); } /// /// Adds bindings for inner text /// /// Type of data source /// Inner tetx binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindInnerText(BindInnerTextOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindInnerText(options); return this; } /// /// Adds bindings for the children collection /// /// Type of elements in the ovservable collection /// Observable collection /// Handler to create elements /// This instance public LaraBuilder BindChildren(ObservableCollection collection, Func creator) { BindingExtensions.BindChildren(_stack.Peek(), collection, _ => creator()); return this; } /// /// Adds bindings for the children collection /// /// Type of elements in observable collection /// Children bindings options /// This instance [Obsolete("Use BindChildren(source, factory) instead")] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindChildren(BindChildrenOptions options) { _stack.Peek().BindChildren(options); return this; } /// /// Adds a two-way binding for an attribute (e.g. 'value' attribute) /// /// Type of data source /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindInput(BindInputOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindInput(options); return this; } /// /// Adds a two-way binding for an attribute (e.g. 'value' attribute) /// /// Type of data source /// Attribute /// Data source /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindInput(string attribute, T data, Expression> property) where T : class, INotifyPropertyChanged { return BindInput(new BindInputOptions { Attribute = attribute, BindObject = data, Property = property }); } /// /// Adds a two-way binding for a flag attribute (e.g. 'checked' attribute) /// /// Type of data source /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindFlagInput(BindFlagInputOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().BindFlagInput(options); return this; } /// /// Adds a two-way binding for a flag attribute (e.g. 'checked' attribute) /// /// Type of data source /// Attribute /// Data source /// Data source property /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder BindFlagInput(string attribute, T data, Expression> property) where T : class, INotifyPropertyChanged { return BindFlagInput(new BindFlagInputOptions { Attribute = attribute, BindObject = data, Property = property }); } /// /// Associates the current element with a data source and an action to update the element whenever the source is modified /// /// Type of the data source /// Data source /// Action to update the element /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder Bind(T instance, Action action) where T : class, INotifyPropertyChanged { return Bind(new BindHandlerOptions { ModifiedHandler = (_, _) => action(), BindObject = instance }); } /// /// Associates the current element with a data source and an action to update the element whenever the source is modified /// /// Type of the data source /// Data source /// Action to update the element /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder Bind(T instance, Action action) where T : class, INotifyPropertyChanged { return Bind(new BindHandlerOptions { ModifiedHandler = action, BindObject = instance }); } /// /// Associates the current element with a data source and an action to update the element whenever the source is modified /// /// Type of the data source /// Binding options /// This instance [Obsolete(ObsoleteElement.BindObsolete)] [EditorBrowsable(EditorBrowsableState.Never)] public LaraBuilder Bind(BindHandlerOptions options) where T : class, INotifyPropertyChanged { _stack.Peek().Bind(options); return this; } #endregion } } ================================================ FILE: src/LaraUI/Tools/LaraJson.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.Serialization; namespace Integrative.Lara { /// /// JSON operations /// public sealed class LaraJson { /// /// Parses a JSON string into a class. The class needs to be decorated with the DataContract attribute. /// /// Class type /// Source JSON string /// Class instance created /// true when successful, false otherwise // ReSharper disable once MemberCanBeMadeStatic.Global public bool TryParse(string json, [NotNullWhen(true)] out T? result) where T : class { try { result = LaraTools.Deserialize(json); return (result != null); } catch (SerializationException) { result = default; return false; } } /// /// Parses a JSON string. If parsing fails, throws a StatusCodeException that returns a Bad Request (400). /// The class needs to be decorated with the DataContract attribute. /// /// Class type /// JSON source text /// Instance of deserialized class public T Parse(string json) where T : class { T? result; try { result = LaraTools.Deserialize(json); } catch (Exception e) { var outer = new StatusCodeException(Resources.BadRequest, e) { StatusCode = HttpStatusCode.BadRequest }; throw outer; } if (result == null) { throw new StatusCodeException(HttpStatusCode.BadRequest, Resources.BadRequest); } return result; } /// /// Serializes a class decorated with DataContract to a JSON string /// /// Type of class /// Instance to serialize /// JSON string public string Stringify(T instance) => LaraTools.Serialize(instance); /// /// Serializes a class decorated with DataContract to a JSON string /// /// Instance to serialize /// Type of class /// JSON string public string Stringify(object instance, Type type) => LaraTools.Serialize(instance, type); internal LaraJson() { } } } ================================================ FILE: src/LaraUI/Tools/LaraOptions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Hosting; namespace Integrative.Lara { /// /// Defines preset modes for applications /// public enum ApplicationMode { /// /// Default mode as a regular web site /// Default, /// /// Launches a browser tab and terminates the host when the user closes away the tab and its child tabs. /// Also, will prevent any further connections besides the one created upon launching the browser. /// BrowserApp } /// /// Lara Web Engine options /// public class LaraOptions { /// /// Looks for classes decorated with Lara attributes and registers them. Default is True. /// public bool PublishAssembliesOnStart { get; set; } /// /// Gets or sets a value indicating whether to allow localhost requests only. /// /// /// true if [allow localhost only]; otherwise, false. /// public bool AllowLocalhostOnly { get; set; } /// /// Gets or sets a value indicating whether Lara will show its default 'not found' page. /// /// /// true if [show not found page]; otherwise, false. /// public bool ShowNotFoundPage { get; set; } = true; /// /// Defines an application mode /// public ApplicationMode Mode { get; set; } = ApplicationMode.Default; /// /// Gets or sets a value indicating whether Lara will include ASP.NET Core WebSocket middleware. Always set to include unless you are manually including the middleware yourself. /// /// /// true if [add web sockets middleware]; otherwise, false. /// public bool AddWebSocketsMiddleware { get; set; } = true; } /// /// Lara options for starting a web server /// /// public class StartServerOptions : LaraOptions { /// /// Gets or sets the port to use when Lara opens a new server. If zero, a dynamic port wil be assigned. /// /// /// The port number. /// public int Port { get; set; } /// /// Gets or sets the IP address where the host is listening. By default, this is the loopback address. /// // ReSharper disable once InconsistentNaming public IPAddress IPAddress { get; set; } = IPAddress.Loopback; /// /// Gets or sets a value indicating whether to show execution exceptions. /// /// /// true if [show exceptions]; otherwise, false. /// // ReSharper disable once MemberCanBePrivate.Global public bool ShowExceptions { get; set; } /// /// Defines an optional method to set additional configuration for the asp.net core instance /// // ReSharper disable once UnusedAutoPropertyAccessor.Global public Action? AdditionalConfiguration { get; set; } /// /// Initializes a new instance of the class. /// public StartServerOptions() { ShowExceptions = Debugger.IsAttached; } } } ================================================ FILE: src/LaraUI/Tools/LaraTools.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization.Json; using System.Text; [assembly: InternalsVisibleTo("Tests")] namespace Integrative.Lara { internal static class LaraTools { public static void LaunchBrowser(IWebHost host) { var address = GetFirstUrl(host); LaunchBrowser(address); } public static string GetFirstUrl(IWebHost webHost) { return webHost.ServerFeatures .Get() .Addresses .First(); } public static void LaunchBrowser(string url) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Process.Start("xdg-open", url); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Process.Start("open", url); } } public static string Serialize(T instance) { return Serialize(instance, typeof(T)); } public static string Serialize(object? instance, Type type) { if (instance == null) { return string.Empty; } var stream = new MemoryStream(); using var reader = new StreamReader(stream); var serializer = new DataContractJsonSerializer(type); serializer.WriteObject(stream, instance); stream.Position = 0; return reader.ReadToEnd(); } public static T? Deserialize(string json) where T : class { using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); return Deserialize(stream); } public static T? Deserialize(Stream stream) where T : class { var serializer = new DataContractJsonSerializer(typeof(T)); return serializer.ReadObject(stream) as T; } public static byte[] Compress(byte[] data) { var output = new MemoryStream(); using (var stream = new DeflateStream(output, CompressionLevel.Fastest)) { stream.Write(data, 0, data.Length); } return output.ToArray(); } public static bool SameValue([AllowNull] T previous, [AllowNull] T value) { if (previous == null) { return value == null; } return previous.Equals(value); } } } ================================================ FILE: src/LaraUI/Tools/NoCurrentSessionException.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara { /// /// The operation requested requires a current session and there isn't one /// public class NoCurrentSessionException : InvalidOperationException { /// /// Constructor /// /// Message public NoCurrentSessionException(string message) : base(message) { } /// /// Constructor /// /// Message /// Inner exception public NoCurrentSessionException(string message, Exception innerException) : base(message, innerException) { } /// /// Constructor /// public NoCurrentSessionException() { } } } ================================================ FILE: src/LaraUI/Tools/SemaphoreSlimExtensions.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Threading; using System.Threading.Tasks; namespace Integrative.Lara { internal static class SemaphoreSlimExtensions { public static async Task UseWaitAsync( this SemaphoreSlim semaphore, CancellationToken cancelToken = default) { await semaphore.WaitAsync(cancelToken); return new ReleaseWrapper(semaphore); } public static IDisposable UseWait(this SemaphoreSlim semaphore, CancellationToken cancelToken = default) { semaphore.Wait(cancelToken); return new ReleaseWrapper(semaphore); } private class ReleaseWrapper : IDisposable { private readonly SemaphoreSlim _semaphore; private bool _disposed; public ReleaseWrapper(SemaphoreSlim semaphore) { _semaphore = semaphore; } public void Dispose() { if (_disposed) return; _disposed = true; _semaphore.Release(); } } } } ================================================ FILE: src/LaraUI/Tools/ServerLauncher.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; namespace Integrative.Lara { internal static class ServerLauncher { public const string ErrorAddress = "/Error"; public static async Task StartServer(Application app, StartServerOptions options) { var host = CreateBrowserHost(app, options); await host.StartAsync(CancellationToken.None); return host; } private static IWebHost CreateBrowserHost(Application laraApp, StartServerOptions options) { var address = options.IPAddress; var port = options.Port; var builder = new WebHostBuilder() .UseKestrel(kestrel => kestrel.Listen(address, port)) .Configure(app => { ConfigureApp(app, laraApp, options); }); options.AdditionalConfiguration?.Invoke(builder); return builder.Build(); } private static void ConfigureApp(IApplicationBuilder app, Application laraApp, StartServerOptions options) { ConfigureExceptions(app, laraApp, options); app.UseLara(laraApp, options); } internal static void ConfigureExceptions(IApplicationBuilder app, Application laraApp, StartServerOptions options) { if (options.ShowExceptions) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler(ErrorAddress); laraApp.ErrorPages.PublishErrorPage(); } } } } ================================================ FILE: src/LaraUI/Tools/SessionLocal.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Integrative.Lara { /// /// Represents ambient data that is local to a given session /// /// Type of data to store public class SessionLocal { private readonly Dictionary _storage; /// /// Constructor /// public SessionLocal() { _storage = new Dictionary(); } /// /// Value /// [MaybeNull] public T Value { get => GetValue(); set => SetValue(value); } private T GetValue() { var session = GetSession(); _storage.TryGetValue(session, out var value); return value; } private void SetValue([AllowNull] T value) { var session = GetSession(); if (_storage.TryGetValue(session, out var previous)) { if (LaraTools.SameValue(previous, value)) { return; } _storage.Remove(session); Store(value, session); } else { Store(value, session); session.CloseComplete += (_, _) => _storage.Remove(session); } } private void Store([AllowNull] T value, Session session) { if (value != null) { _storage.Add(session, value); } } private static Session GetSession() { if (LaraUI.Context is IPageContext page) { return page.Session; } if (LaraUI.Context is IWebServiceContext service && service.TryGetSession(out var session)) { return session; } throw new NoCurrentSessionException(Resources.NoCurrentSession); } } } ================================================ FILE: src/LaraUI/docfx.json ================================================ { "metadata": [ { "src": [ { "files": [ "**.csproj" ], "src": "C:\\Users\\Pablo\\OneDrive\\2019\\LaraUI\\src\\LaraUI" } ], "dest": "api", "disableGitFeatures": false, "disableDefaultFilter": false } ], "build": { "content": [ { "files": [ "api/**.yml", "api/index.md" ] }, { "files": [ "articles/**.md", "articles/**/toc.yml", "toc.yml", "*.md" ] } ], "resource": [ { "files": [ "images/**" ] } ], "overwrite": [ { "files": [ "apidoc/**.md" ], "exclude": [ "obj/**", "_site/**" ] } ], "dest": "_site", "globalMetadataFiles": [], "fileMetadataFiles": [], "template": [ "default" ], "postProcessors": [], "markdownEngineName": "markdig", "noLangKeyword": false, "keepFileLink": false, "cleanupCacheHistory": false, "disableGitFeatures": false } } ================================================ FILE: src/LaraUI/pack.bat ================================================ dotnet pack -c RELEASE ================================================ FILE: src/LaraUI.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.452 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LaraUI", "LaraUI\LaraUI.csproj", "{1CF006B1-8B28-45BC-B9C8-1E548ACD115D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleProject", "SampleProject\SampleProject.csproj", "{C40FC41C-4FED-4502-A912-33BCE42954AD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0518A1A9-5F3F-4081-96E4-68B771F81A26}" ProjectSection(SolutionItems) = preProject ..\.gitignore = ..\.gitignore LaraUI.sln.licenseheader = LaraUI.sln.licenseheader EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{E93241E3-DCE0-47AF-BA01-2F1C02864F67}" EndProject Project("{7CF6DF6D-3B04-46F8-A40B-537D21BCA0B4}") = "LaraDocumentation", "LaraDocumentation\LaraDocumentation.shfbproj", "{0019560B-ED95-4921-9050-8AD37BF9D7EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WikiExamples", "Boilerplate\WikiExamples.csproj", "{BB775C40-4D4B-4B5E-A665-07653EC6EFD6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Debug|x64.ActiveCfg = Debug|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Debug|x64.Build.0 = Debug|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Release|Any CPU.Build.0 = Release|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Release|x64.ActiveCfg = Release|Any CPU {1CF006B1-8B28-45BC-B9C8-1E548ACD115D}.Release|x64.Build.0 = Release|Any CPU {C40FC41C-4FED-4502-A912-33BCE42954AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C40FC41C-4FED-4502-A912-33BCE42954AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {C40FC41C-4FED-4502-A912-33BCE42954AD}.Debug|x64.ActiveCfg = Debug|x64 {C40FC41C-4FED-4502-A912-33BCE42954AD}.Debug|x64.Build.0 = Debug|x64 {C40FC41C-4FED-4502-A912-33BCE42954AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {C40FC41C-4FED-4502-A912-33BCE42954AD}.Release|Any CPU.Build.0 = Release|Any CPU {C40FC41C-4FED-4502-A912-33BCE42954AD}.Release|x64.ActiveCfg = Release|x64 {C40FC41C-4FED-4502-A912-33BCE42954AD}.Release|x64.Build.0 = Release|x64 {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Debug|Any CPU.Build.0 = Debug|Any CPU {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Debug|x64.ActiveCfg = Debug|x64 {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Debug|x64.Build.0 = Debug|x64 {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Release|Any CPU.ActiveCfg = Release|Any CPU {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Release|Any CPU.Build.0 = Release|Any CPU {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Release|x64.ActiveCfg = Release|x64 {E93241E3-DCE0-47AF-BA01-2F1C02864F67}.Release|x64.Build.0 = Release|x64 {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Debug|x64.ActiveCfg = Debug|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Debug|x64.Build.0 = Debug|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Release|Any CPU.Build.0 = Release|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Release|x64.ActiveCfg = Release|Any CPU {0019560B-ED95-4921-9050-8AD37BF9D7EF}.Release|x64.Build.0 = Release|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Debug|x64.ActiveCfg = Debug|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Debug|x64.Build.0 = Debug|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Release|Any CPU.Build.0 = Release|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Release|x64.ActiveCfg = Release|Any CPU {BB775C40-4D4B-4B5E-A665-07653EC6EFD6}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2ADA43B8-3C29-40D4-BF2A-32BFC3C0D173} EndGlobalSection EndGlobal ================================================ FILE: src/LaraUI.sln.licenseheader ================================================ extensions: designer.cs generated.cs extensions: .cs .cpp .h .js .ts /* Copyright (c) %CurrentYear% Integrative Software LLC Created: %CreationMonth%/%CreationYear% Author: Pablo Carbonell */ extensions: .aspx .ascx <%-- Copyright (c) %CurrentYear% Integrative Software LLC Created: %CreationMonth%/%CreationYear% Author: Pablo Carbonell --%> extensions: .vb 'Copyright (c) %CurrentYear% Integrative Software LLC 'Created: %CreationMonth%/%CreationYear% 'Author: Pablo Carbonell extensions: .xml .config .xsd ================================================ FILE: src/SampleProject/Common/CountryList.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using System.Collections.Generic; namespace SampleProject.Common { internal class Country { public Country(string code, string name) { Code = code; Name = name; } public string Code { get; init; } public string Name { get; init; } } internal static class CountryList { private static readonly Dictionary _Countries = new Dictionary() { { "AF", "Afghanistan" }, { "AL", "Albania" }, { "DZ", "Algeria" }, { "AS", "American Samoa" }, { "AD", "Andorra" }, { "AO", "Angola" }, { "AI", "Anguilla" }, { "AQ", "Antarctica" }, { "AG", "Antigua And Barbuda" }, { "AR", "Argentina" }, { "AM", "Armenia" }, { "AW", "Aruba" }, { "AU", "Australia" }, { "AT", "Austria" }, { "AZ", "Azerbaijan" }, { "BS", "Bahamas" }, { "BH", "Bahrain" }, { "BD", "Bangladesh" }, { "BB", "Barbados" }, { "BY", "Belarus" }, { "BE", "Belgium" }, { "BZ", "Belize" }, { "BJ", "Benin" }, { "BM", "Bermuda" }, { "BT", "Bhutan" }, { "BO", "Bolivia" }, { "BA", "Bosnia And Herzegovina" }, { "BW", "Botswana" }, { "BV", "Bouvet Island" }, { "BR", "Brazil" }, { "IO", "British Indian Ocean Territory" }, { "BN", "Brunei Darussalam" }, { "BG", "Bulgaria" }, { "BF", "Burkina Faso" }, { "BI", "Burundi" }, { "KH", "Cambodia" }, { "CM", "Cameroon" }, { "CA", "Canada" }, { "CV", "Cape Verde" }, { "KY", "Cayman Islands" }, { "CF", "Central African Republic" }, { "TD", "Chad" }, { "CL", "Chile" }, { "CN", "China" }, { "CX", "Christmas Island" }, { "CC", "Cocos (keeling) Islands" }, { "CO", "Colombia" }, { "KM", "Comoros" }, { "CG", "Congo" }, { "CD", "Congo, The Democratic Republic Of The" }, { "CK", "Cook Islands" }, { "CR", "Costa Rica" }, { "CI", "Cote D'ivoire" }, { "HR", "Croatia" }, { "CU", "Cuba" }, { "CY", "Cyprus" }, { "CZ", "Czech Republic" }, { "DK", "Denmark" }, { "DJ", "Djibouti" }, { "DM", "Dominica" }, { "DO", "Dominican Republic" }, { "TP", "East Timor" }, { "EC", "Ecuador" }, { "EG", "Egypt" }, { "SV", "El Salvador" }, { "GQ", "Equatorial Guinea" }, { "ER", "Eritrea" }, { "EE", "Estonia" }, { "ET", "Ethiopia" }, { "FK", "Falkland Islands (malvinas)" }, { "FO", "Faroe Islands" }, { "FJ", "Fiji" }, { "FI", "Finland" }, { "FR", "France" }, { "GF", "French Guiana" }, { "PF", "French Polynesia" }, { "TF", "French Southern Territories" }, { "GA", "Gabon" }, { "GM", "Gambia" }, { "GE", "Georgia" }, { "DE", "Germany" }, { "GH", "Ghana" }, { "GI", "Gibraltar" }, { "GR", "Greece" }, { "GL", "Greenland" }, { "GD", "Grenada" }, { "GP", "Guadeloupe" }, { "GU", "Guam" }, { "GT", "Guatemala" }, { "GN", "Guinea" }, { "GW", "Guinea-bissau" }, { "GY", "Guyana" }, { "HT", "Haiti" }, { "HM", "Heard Island And Mcdonald Islands" }, { "VA", "Holy See (vatican City State)" }, { "HN", "Honduras" }, { "HK", "Hong Kong" }, { "HU", "Hungary" }, { "IS", "Iceland" }, { "IN", "India" }, { "ID", "Indonesia" }, { "IR", "Iran, Islamic Republic Of" }, { "IQ", "Iraq" }, { "IE", "Ireland" }, { "IL", "Israel" }, { "IT", "Italy" }, { "JM", "Jamaica" }, { "JP", "Japan" }, { "JO", "Jordan" }, { "KZ", "Kazakstan" }, { "KE", "Kenya" }, { "KI", "Kiribati" }, { "KP", "Korea, Democratic People's Republic Of" }, { "KR", "Korea, Republic Of" }, { "KV", "Kosovo" }, { "KW", "Kuwait" }, { "KG", "Kyrgyzstan" }, { "LA", "Lao People's Democratic Republic" }, { "LV", "Latvia" }, { "LB", "Lebanon" }, { "LS", "Lesotho" }, { "LR", "Liberia" }, { "LY", "Libyan Arab Jamahiriya" }, { "LI", "Liechtenstein" }, { "LT", "Lithuania" }, { "LU", "Luxembourg" }, { "MO", "Macau" }, { "MK", "Macedonia, The Former Yugoslav Republic Of" }, { "MG", "Madagascar" }, { "MW", "Malawi" }, { "MY", "Malaysia" }, { "MV", "Maldives" }, { "ML", "Mali" }, { "MT", "Malta" }, { "MH", "Marshall Islands" }, { "MQ", "Martinique" }, { "MR", "Mauritania" }, { "MU", "Mauritius" }, { "YT", "Mayotte" }, { "MX", "Mexico" }, { "FM", "Micronesia, Federated States Of" }, { "MD", "Moldova, Republic Of" }, { "MC", "Monaco" }, { "MN", "Mongolia" }, { "MS", "Montserrat" }, { "ME", "Montenegro" }, { "MA", "Morocco" }, { "MZ", "Mozambique" }, { "MM", "Myanmar" }, { "NA", "Namibia" }, { "NR", "Nauru" }, { "NP", "Nepal" }, { "NL", "Netherlands" }, { "AN", "Netherlands Antilles" }, { "NC", "New Caledonia" }, { "NZ", "New Zealand" }, { "NI", "Nicaragua" }, { "NE", "Niger" }, { "NG", "Nigeria" }, { "NU", "Niue" }, { "NF", "Norfolk Island" }, { "MP", "Northern Mariana Islands" }, { "NO", "Norway" }, { "OM", "Oman" }, { "PK", "Pakistan" }, { "PW", "Palau" }, { "PS", "Palestinian Territory, Occupied" }, { "PA", "Panama" }, { "PG", "Papua New Guinea" }, { "PY", "Paraguay" }, { "PE", "Peru" }, { "PH", "Philippines" }, { "PN", "Pitcairn" }, { "PL", "Poland" }, { "PT", "Portugal" }, { "PR", "Puerto Rico" }, { "QA", "Qatar" }, { "RE", "Reunion" }, { "RO", "Romania" }, { "RU", "Russian Federation" }, { "RW", "Rwanda" }, { "SH", "Saint Helena" }, { "KN", "Saint Kitts And Nevis" }, { "LC", "Saint Lucia" }, { "PM", "Saint Pierre And Miquelon" }, { "VC", "Saint Vincent And The Grenadines" }, { "WS", "Samoa" }, { "SM", "San Marino" }, { "ST", "Sao Tome And Principe" }, { "SA", "Saudi Arabia" }, { "SN", "Senegal" }, { "RS", "Serbia" }, { "SC", "Seychelles" }, { "SL", "Sierra Leone" }, { "SG", "Singapore" }, { "SK", "Slovakia" }, { "SI", "Slovenia" }, { "SB", "Solomon Islands" }, { "SO", "Somalia" }, { "ZA", "South Africa" }, { "GS", "South Georgia And The South Sandwich Islands" }, { "ES", "Spain" }, { "LK", "Sri Lanka" }, { "SD", "Sudan" }, { "SR", "Suriname" }, { "SJ", "Svalbard And Jan Mayen" }, { "SZ", "Swaziland" }, { "SE", "Sweden" }, { "CH", "Switzerland" }, { "SY", "Syrian Arab Republic" }, { "TW", "Taiwan, Province Of China" }, { "TJ", "Tajikistan" }, { "TZ", "Tanzania, United Republic Of" }, { "TH", "Thailand" }, { "TG", "Togo" }, { "TK", "Tokelau" }, { "TO", "Tonga" }, { "TT", "Trinidad And Tobago" }, { "TN", "Tunisia" }, { "TR", "Turkey" }, { "TM", "Turkmenistan" }, { "TC", "Turks And Caicos Islands" }, { "TV", "Tuvalu" }, { "UG", "Uganda" }, { "UA", "Ukraine" }, { "AE", "United Arab Emirates" }, { "GB", "United Kingdom" }, { "US", "United States" }, { "UM", "United States Minor Outlying Islands" }, { "UY", "Uruguay" }, { "UZ", "Uzbekistan" }, { "VU", "Vanuatu" }, { "VE", "Venezuela" }, { "VN", "Viet Nam" }, { "VG", "Virgin Islands, British" }, { "VI", "Virgin Islands, U.s." }, { "WF", "Wallis And Futuna" }, { "EH", "Western Sahara" }, { "YE", "Yemen" }, { "ZM", "Zambia" }, { "ZW", "Zimbabwe" }, }; public static IEnumerable SearchCountries(string term) { if (string.IsNullOrEmpty(term)) yield break; var upper = term.ToUpperInvariant(); foreach (var pair in _Countries) { var nameUpper = pair.Value.ToUpperInvariant(); if (nameUpper.Contains(upper)) { yield return new Country(pair.Key, pair.Value); } } } } } ================================================ FILE: src/SampleProject/Common/CountrySelector.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; using System.Collections.Generic; using System.Threading.Tasks; namespace SampleProject.Common { internal class CountrySelector : WebComponent, IAutocompleteProvider { public CountrySelector() { var auto = new AutocompleteElement { Class = "form-control" }; var span = new HtmlSpanElement(); ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row mt-3", Children = new Node[] { new HtmlDivElement { Class = "form-group col-md-3", Children = new Node[] { new HtmlLabelElement { InnerText = "Select country" }, auto } } } }, new HtmlDivElement { Class = "form-row mb-2", Children = new Node[] { new HtmlButtonElement { InnerText = "Show country code", Class = "btn btn-primary" } .Event("click", () => span.InnerText = auto.Value), new HtmlDivElement { Class = "ml-2 pt-2", Children = new Node[] { span } } } }, }; auto.Start(new AutocompleteOptions { AutoFocus = true, MinLength = 2, Strict = true, Provider = this }); } public Task GetAutocompleteList(string term) { var suggestions = new List(); foreach (var country in CountryList.SearchCountries(term)) { suggestions.Add(new AutocompleteEntry { Label = country.Name, Code = country.Code }); } var result = new AutocompleteResponse { Suggestions = suggestions }; return Task.FromResult(result); } } } ================================================ FILE: src/SampleProject/Common/SampleAppBootstrap.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara; namespace SampleProject.Common { internal static class SampleAppBootstrap { public static void AppendTo(Element head) { head.AppendChild(new HtmlLinkElement { Rel = "stylesheet", HRef = "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" }); head.AppendChild(new HtmlScriptElement { Src = "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", Defer = true }); head.AppendChild(new HtmlScriptElement { Src = "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js", Defer = true }); } } } ================================================ FILE: src/SampleProject/Common/Tools.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using Integrative.Lara; using System; using System.Reflection; namespace SampleProject.Common { internal class Tools { public static byte[] LoadEmbeddedResource(Assembly assembly, string resourceName) { using var stream = assembly.GetManifestResourceStream(resourceName); if (stream == null) throw new InvalidOperationException($"Resource not found: {resourceName}"); var bytes = new byte[stream.Length]; stream.Read(bytes, 0, bytes.Length); return bytes; } public static string GetSpinnerHtml(string message) { var div = new HtmlDivElement { Class = "d-flex justify-content-center", Children = new Node[] { new HtmlDivElement { Class = "spinner-border", }, new HtmlDivElement { Class = "ml-2", InnerText = message } } }; return div.GetHtml(); } } } ================================================ FILE: src/SampleProject/Components/CheckboxSample.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara; using System.Threading.Tasks; namespace SampleProject.Components { internal class CheckboxSample : WebComponent { public CheckboxSample() { var checkbox = new HtmlInputElement { Id = "mycheckbox", Type = "checkbox", Class = "form-check-input" }; var toggle = new HtmlButtonElement { Class = "btn btn-primary", InnerText = "Toggle" }; toggle.On("click", () => { checkbox.Checked = !checkbox.Checked; return Task.CompletedTask; }); ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row", Children = new Node[] { new HtmlDivElement { Class = "form-group col-md-2 my-1", Children = new Node[] { new HtmlDivElement { Class = "form-check", Children = new Node[] { checkbox, new HtmlLabelElement { For = checkbox.Id, InnerText = "Check me out" } } } } }, new HtmlDivElement { Class = "form-group col-md-1", Children = new Node[] { toggle } } } } }; } } } ================================================ FILE: src/SampleProject/Components/CounterSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 1/2020 Author: Pablo Carbonell */ using Integrative.Lara; namespace SampleProject.Components { internal class CounterSample : WebComponent { private int _counter = 5; private int Counter { get => _counter; set => SetProperty(ref _counter, value); } private static int TextToInt(string? value) { return int.TryParse(value, out var result) ? result : 0; } public CounterSample() { ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row", Children = new Node[] { new HtmlDivElement { Class = "form-group col-md-2", Children = new Node[] { new HtmlInputElement { Type = "number", Class = "form-control" } .Bind(this, x => x.Value = Counter.ToString()) .BindBack(x => Counter = TextToInt(x.Value)) } }, new HtmlDivElement { Class = "form-group col-md-1", Children = new Node[] { new HtmlButtonElement { InnerText = "Increase", Class = "btn btn-primary" } .Event("click", () => Counter++) } } } } }; } } } ================================================ FILE: src/SampleProject/Components/KitchenSinkComponent.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; using SampleProject.Common; namespace SampleProject.Components { internal class KitchenSinkComponent : WebComponent { public KitchenSinkComponent() { ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "container p-4", Children = new Node[] { new CounterSample(), new CheckboxSample(), new MultiselectSample(), new LockingSample(), new LongRunningSample(), new CountrySelector(), new HtmlDivElement { Class = "mt-3", Children = new Node[] { new HtmlDivElement { Class = "mt-2", Children = new Node[] { new HtmlDivElement { Children = new Node[] { new HtmlAnchorElement { HRef = "/upload", Target = "_blank", InnerText = "File upload example" } } }, new HtmlDivElement { Children = new Node[] { new HtmlAnchorElement { HRef = "/server", Target = "_blank", InnerText = "Server-side events" } } } } }, } } } } }; } } } ================================================ FILE: src/SampleProject/Components/LockingSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; using SampleProject.Common; using System.Threading.Tasks; namespace SampleProject.Components { internal class LockingSample : WebComponent { public LockingSample() { ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row", Children = new Node[] { new HtmlButtonElement { Class = "btn btn-primary my-2", InnerText = "Action that blocks the UI" } .Event(new EventSettings { EventName = "click", Handler = () => Task.Delay(1000), BlockOptions = new BlockOptions { ShowHtmlMessage = Tools.GetSpinnerHtml("Brewing coffee...") } }) } }, }; } } } ================================================ FILE: src/SampleProject/Components/LongRunningSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 1/2020 Author: Pablo Carbonell */ using Integrative.Lara; using System.Threading.Tasks; namespace SampleProject.Components { internal class LongRunningSample : WebComponent { private static readonly string[] _Steps = { "Putting grounds into container", "Covering coffee and water mixture", "Filtering coffee and water mixture", "Serving..." }; public LongRunningSample() { ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row", Children = new Node[] { new HtmlButtonElement { Class = "btn btn-primary my-2", InnerText = "Long-running action" } .Extract(out var button) } }, new HtmlDivElement { Class = "card text-center", Style = "display: none; width: 18rem", Children = new Node[] { new HtmlImageElement { Class = "card-img-top mt-2", Height = "100", Src = "/Coffee.svg", Alt = "Coffee mug" }, new HtmlDivElement { Class = "card-body", Children = new Node[] { new HtmlHeadingElement(5) { Class = "card-title", InnerText = "Preparing..." }, new HtmlParagraphElement { Class = "card=-text" } .Extract(out var text) } } } } .Extract(out var card) }; button.On(new EventSettings { EventName = "click", LongRunning = true, BlockOptions = new BlockOptions { ShowElementId = card.Id }, Handler = async () => { foreach (var step in _Steps) { text.InnerText = step; await LaraUI.FlushPartialChanges(); await Task.Delay(800); } text.ClearChildren(); } }); } } } ================================================ FILE: src/SampleProject/Components/MultiselectSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; namespace SampleProject.Components { internal class MultiselectSample : WebComponent { public MultiselectSample() { var combo = new HtmlSelectElement { Class = "form-control", Multiple = true }; combo.AddOption("N", "North"); combo.AddOption("E", "East"); combo.AddOption("S", "South"); combo.AddOption("W", "West"); var toggle = new HtmlButtonElement { Class = "btn btn-primary", InnerText = "Toggle" }; toggle.On("click", () => { foreach (var child in combo.Children) { if (child is not HtmlOptionElement option) continue; option.Selected = !option.Selected; } }); ShadowRoot.Children = new Node[] { new HtmlDivElement { Class = "form-row", Children = new Node[] { new HtmlDivElement { Class = "form-group col-md-2", Children = new Node[] { combo } }, new HtmlDivElement { Class = "form-group col-md-1", Children = new Node[] { toggle } } } } }; } } } ================================================ FILE: src/SampleProject/Components/SelectSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; namespace SampleProject.Components { internal class SelectSample : WebComponent { public int Test { get; init; } public SelectSample() { ShadowRoot.Child( new HtmlDivElement { Class = "form-row" }.Child( new HtmlDivElement { Class = "form-group col-md-2" }.Child( new WeekdayCombo().Extract(out var combo) ), new HtmlDivElement { Class = "form-group col-md-1"}.Child( new HtmlButtonElement { InnerText = "Advance", Class ="btn btn-primary" } .Event("click", () => combo.NextDay()) ) )); } } } ================================================ FILE: src/SampleProject/Components/UploadSample.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; using System.Collections.Generic; using System.Threading.Tasks; namespace SampleProject.Components { internal class UploadSample : WebComponent { public UploadSample() { var file = new HtmlInputElement { Type = "file", Multiple = true, Style = "margin: 5px" }; var span = new HtmlSpanElement(); ShadowRoot.Children = new Node[] { new HtmlDivElement { Children = new Node[] { file } }, new HtmlDivElement { Children = new Node[] { new HtmlButtonElement { InnerText = "Upload via Ajax", Style = "margin: 5px" } .Event(new EventSettings { EventName = "click", UploadFiles = true, Handler = () => { span.InnerText = GetUploadText(file); return Task.CompletedTask; }, }), new HtmlButtonElement { InnerText = "Upload via Websocket", Style = "margin: 5px" } .Event(new EventSettings { EventName = "click", UploadFiles = true, Handler = () => { span.InnerText = GetUploadText(file); return Task.CompletedTask; }, LongRunning = true }), } }, new HtmlDivElement { Style = "margin: 5px", Children = new Node[] { span } } }; } private static string GetUploadText(HtmlInputElement input) { if (input.Files.Count == 0) { return "No files uploaded"; } return "Uploaded: " + string.Join(", ", GetFileNames(input)); } private static IEnumerable GetFileNames(HtmlInputElement input) { foreach (var file in input.Files) { var text = $"{file.FileName} ({file.Length} bytes)"; yield return text; } } } } ================================================ FILE: src/SampleProject/Components/WeekdayCombo.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; namespace SampleProject.Components { public class WeekdayCombo : WebComponent { private static readonly string[] _WeekDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; private int _weekday; private int Weekday { get => _weekday; set => SetProperty(ref _weekday, value); } public WeekdayCombo() { var combo = new HtmlSelectElement { Class = "form-control", Id = "MyWeek" }; for (var index = 0; index < _WeekDays.Length; index++) { combo.AddOption(index.ToString(), _WeekDays[index]); } combo.Bind(this, _ => combo.Value = Weekday.ToString()); combo.BindBack(_ => Weekday = int.Parse(combo.Value ?? "")); ShadowRoot.Children = new[] { combo }; } public void NextDay() => Weekday = (Weekday + 1) % _WeekDays.Length; } } ================================================ FILE: src/SampleProject/LaraSample.csproj ================================================  netcoreapp2.1 x64;AnyCPU win-x64;osx-x64;linux-x64 WinExe latest ================================================ FILE: src/SampleProject/Main/Program.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara; using SampleProject.Pages; using System; using System.Threading.Tasks; namespace SampleProject.Main { internal static class Program { private static async Task Main() { // create application using var app = new Application(); KitchenSinkPage.PublishMugImage(app); app.PublishPage("/", () => new KitchenSinkPage()); app.PublishPage("/upload", () => new UploadFilePage()); app.PublishPage("/server", () => new ServerEventsPage()); // start application await app.Start(new StartServerOptions { Port = 8182 }); Console.WriteLine("Listening on http://localhost:8182/"); LaraUI.LaunchBrowser("http://localhost:8182"); // wait for shutdown await app.WaitForShutdown(); } } } ================================================ FILE: src/SampleProject/Pages/KitchenSinkPage.cs ================================================ /* Copyright (c) 2020-2021 Integrative Software LLC Created: 12/2020 Author: Pablo Carbonell */ using Integrative.Lara; using SampleProject.Components; using SampleProject.Common; using System.Threading.Tasks; namespace SampleProject.Pages { internal class KitchenSinkPage : IPage { public Task OnGet() { var document = LaraUI.Page.Document; // This sample application loads the CSS library 'Bootstrap' SampleAppBootstrap.AppendTo(document.Head); document.Body.AppendChild(new KitchenSinkComponent()); return Task.CompletedTask; } public static void PublishMugImage(Application app) { var assembly = typeof(KitchenSinkPage).Assembly; var bytes = Tools.LoadEmbeddedResource(assembly, "SampleProject.Assets.Coffee.svg"); app.PublishFile("/Coffee.svg", new StaticContent(bytes, "image/svg+xml")); } } } ================================================ FILE: src/SampleProject/Pages/ServerEventsPage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; using Integrative.Lara; namespace SampleProject.Pages { internal class ServerEventsPage : IPage { private readonly HtmlSpanElement _span; public ServerEventsPage() { _span = new HtmlSpanElement(); _span.AppendText("waiting for server event..."); } public Task OnGet() { LaraUI.Page.JSBridge.ServerEventsOn(); LaraUI.Page.Document.Body.AppendChild(_span); Task.Run(DelayedTask); return Task.CompletedTask; } private async void DelayedTask() { await Task.Delay(4000); using var access = LaraUI.Document.StartServerEvent(); _span.ClearChildren(); _span.AppendText("server event executed"); } } } ================================================ FILE: src/SampleProject/Pages/UploadFilePage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using Integrative.Lara; using SampleProject.Components; using System.Threading.Tasks; namespace SampleProject.Pages { internal class UploadFilePage : IPage { public Task OnGet() { LaraUI.Document.Body.AppendChild(new UploadSample()); return Task.CompletedTask; } } } ================================================ FILE: src/SampleProject/Properties/launchSettings.json ================================================ { "profiles": { "SampleProject": { "commandName": "Project" } } } ================================================ FILE: src/SampleProject/SampleProject.csproj ================================================  net5.0 x64;AnyCPU win-x64;osx-x64;linux-x64 Exe latest enable ================================================ FILE: src/Tests/Components/AutocompleteTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using Moq; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Components { internal class MyProvider : IAutocompleteProvider { public Task GetAutocompleteList(string term) { var list = new List(); var response = new AutocompleteResponse { Suggestions = list }; list.Add(new AutocompleteEntry { Code = "R", Label = "Red" }); list.Add(new AutocompleteEntry { Code = "G", Label = "Green" }); list.Add(new AutocompleteEntry { Code = "B", Label = "Blue" }); return Task.FromResult(response); } } public class AutocompleteTesting : DummyContextTesting { public AutocompleteTesting() { Context.Application.PublishComponent(new WebComponentOptions { ComponentTagName = AutocompleteElement.CustomTag, ComponentType = typeof(AutocompleteElement) }); } [Fact] public void InnerInputValue() { var x = new AutocompleteElement { Value = "abc" }; Assert.Equal("abc", x.Value); Assert.Equal("abc", x.InnerInput.Value); } [Fact] public void AutocompleteOptionsStore() { var provider = new MyProvider(); var options = new AutocompleteOptions { Provider = provider, AutoFocus = true, MinLength = 2, Strict = true }; Assert.Same(provider, options.Provider); Assert.True(options.AutoFocus); Assert.Equal(2, options.MinLength); Assert.True(options.Strict); } [Fact] public void AutocompleteStarts() { LaraUI.InternalContext.Value = Context; var x = new AutocompleteElement(); var provider = new MyProvider(); var options = new AutocompleteOptions { Provider = provider, AutoFocus = true, MinLength = 2, Strict = true }; x.Start(options); var doc = new Document(new MyPage(), 100); var bridge = new Mock(); Context.JSBridge = bridge.Object; const string code = "LaraUI.autocompleteApply(context.Payload);"; var payload = new AutocompletePayload { AutoFocus = options.AutoFocus, ElementId = x.InnerInput.Id, MinLength = options.MinLength, Strict = options.Strict }; var json = LaraUI.JSON.Stringify(payload); bridge.Setup(x1 => x1.Submit(code, json)); doc.Body.AppendChild(x); bridge.Verify(x2 => x2.Submit(code, json), Times.Once); } [Fact] public void AutocompleteStartStop() { LaraUI.InternalContext.Value = Context; var x = new AutocompleteElement(); var provider = new MyProvider(); var options = new AutocompleteOptions { Provider = provider, AutoFocus = true, MinLength = 2, Strict = true }; var doc = new Document(new MyPage(), 100); var bridge = new Mock(); Context.JSBridge = bridge.Object; doc.Body.AppendChild(x); x.Start(options); Assert.Equal(1, AutocompleteService.RegisteredCount); x.Stop(); Assert.Equal(0, AutocompleteService.RegisteredCount); } [Fact] public void OnDisconnectStops() { LaraUI.InternalContext.Value = Context; var x = new AutocompleteElement(); var provider = new MyProvider(); var options = new AutocompleteOptions { Provider = provider, AutoFocus = true, MinLength = 2, Strict = true }; var doc = new Document(new MyPage(), 100); var bridge = new Mock(); Context.JSBridge = bridge.Object; doc.Body.AppendChild(x); x.Start(options); Assert.Equal(1, AutocompleteService.RegisteredCount); Assert.Same(options, x.GetOptions()); x.Remove(); Assert.Equal(0, AutocompleteService.RegisteredCount); } [Fact] public void AutocompleteEntry() { var x = new AutocompleteEntry { Code = "a", Html = "b", Label = "c", Subtitle = "d" }; Assert.Equal("a", x.Code); Assert.Equal("b", x.Html); Assert.Equal("c", x.Label); Assert.Equal("d", x.Subtitle); } [Fact] public void AutocompleteResponse() { var list = new List(); var x = new AutocompleteResponse { Suggestions = list }; Assert.Same(list, x.Suggestions); } [Fact] public async void AutocompleteServiceRun() { LaraUI.InternalContext.Value = Context; var x = new AutocompleteElement(); var provider = new MyProvider(); var options = new AutocompleteOptions { Provider = provider, AutoFocus = true, MinLength = 2, Strict = true, }; var doc = new Document(new MyPage(), 100); var bridge = new Mock(); Context.JSBridge = bridge.Object; doc.Body.AppendChild(x); x.Start(options); var service = new AutocompleteService(); var request = new AutocompleteRequest { Key = x.AutocompleteId, Term = "B" }; Context.RequestBody = LaraUI.JSON.Stringify(request); var text = await service.Execute(); var response = LaraUI.JSON.Parse(text); Assert.Equal(3, response.Suggestions!.Count); var item = response.Suggestions[0]; Assert.Equal("Red", item.Label); Assert.Equal("R", item.Code); } [Fact] public void RegistryReplacesEntries() { LaraUI.InternalContext.Value = Context; var x = new AutocompleteRegistry(); var auto1 = new AutocompleteElement(); var auto2 = new AutocompleteElement(); x.Set("a", auto1); x.Set("a", auto2); Assert.True(x.TryGet("a", out var autoX)); Assert.Same(auto2, autoX); } [Fact] public async void ExecuteNotFoundReturnsEmpty() { var request = new AutocompleteRequest { Key = "a", Term = "" }; var json = LaraUI.JSON.Stringify(request); var result = await AutocompleteService.Execute(json); Assert.Equal(string.Empty, result); } } } ================================================ FILE: src/Tests/Components/ComponentTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using Moq; using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Components { [LaraWebComponent("x-dummy")] internal class MyDummyComponent : WebComponent { public int Moved { get; private set; } public MyDummyComponent() : base("x-dummy") { } protected override IEnumerable GetObservedAttributes() { return new[] { "class" }; } protected override void OnMove() { base.OnMove(); Moved++; } } [LaraWebComponent("x-com")] [Obsolete("Old methods")] internal class Xcom : WebComponent { public Xcom() : base("x-com") { var builder = new LaraBuilder(ShadowRoot); builder.Push("div", "", "div1") .Push("div", "", "div1a") .Pop() .Pop() .Push("div", "", "div2") .Pop() .AppendText("lalas"); } } [LaraWebComponent("x-light")] internal class LightCom : WebComponent { public LightCom() : base("x-light") { } } [LaraWebComponent("x-slotter")] internal class MySlotter : WebComponent { public MySlotter() : base("x-slotter") { var div = Create("div"); var slot = Create("slot"); ShadowRoot.AppendChild(div); div.AppendChild(slot); } } [LaraWebComponent("x-twodiv")] internal class MyTwoDivComponent : WebComponent { public MyTwoDivComponent(bool useShadow) : base("x-twodiv") { if (!useShadow) return; ShadowRoot.AppendChild(Create("div")); ShadowRoot.AppendText("hello"); } } [LaraWebComponent("x-obsolete")] internal class ObsoleteComponent : WebComponent { public ObsoleteComponent() : base("x-obsolete") { } public int Counter { get; private set; } [Obsolete] public void Test() { AttachShadow(); Counter++; } } public class ComponentTesting : IDisposable { private readonly Application _app; public ComponentTesting() { _app = new Application(); _app.PublishAssemblies(); var http = new Mock(); var connection = new Connection(Guid.NewGuid(), IPAddress.Loopback); var context = new PageContext(_app, http.Object, connection); LaraUI.InternalContext.Value = context; } public void Dispose() { _app.Dispose(); GC.SuppressFinalize(this); } [Fact] public void RegisterComponentSucceeds() { _app.PublishComponent(new WebComponentOptions { ComponentTagName = "x-caca", ComponentType = typeof(MyComponent) }); Assert.True(LaraUI.Context.Application.TryGetComponent("x-caca", out var type)); _app.UnPublishWebComponent("x-caca"); Assert.Equal(typeof(MyComponent), type); Assert.False(LaraUI.Context.Application.TryGetComponent("x-caca", out _)); } private class MyComponent : WebComponent { public MyComponent() : base("x-caca") { } } private class MyPage : IPage { public Task OnGet() { return Task.CompletedTask; } } [Fact] public void ServerEventsOnSucceeds() { var context = CreateMockPage(); Assert.Equal(ServerEventsStatus.Disabled, context.Document.ServerEventsStatus); context.JSBridge.ServerEventsOn(); Assert.Equal(ServerEventsStatus.Connecting, context.Document.ServerEventsStatus); context.JSBridge.ServerEventsOff(); Assert.Equal(ServerEventsStatus.Disabled, context.Document.ServerEventsStatus); } private PageContext CreateMockPage() { var http = new Mock(); var guid = Guid.Parse("{5166FB58-FB45-4622-90E6-195E2448F2C9}"); var connection = new Connection(guid, IPAddress.Loopback); var document = new Document(new MyPage(), 100); return new PageContext(_app, http.Object, connection) { DocumentInternal = document }; } [Fact] [Obsolete("Old methods")] public void WebComponentListsAllDescendents() { var x = new Xcom(); var div = Element.Create("div"); div.Id = "lala"; x.AppendChild(div); var set = new HashSet(); foreach (var node in GetAllDescendents(x)) { if (node is Element child && !string.IsNullOrEmpty(child.Id)) { set.Add(child.Id); } } Assert.Contains("div1", set); Assert.Contains("div2", set); Assert.Contains("div1a", set); Assert.Contains("lala", set); } [Fact] [Obsolete("Old methods")] public void FlattenedChildrenIncludesPrintedOnes() { var container = Element.Create("div"); var x = new Xcom(); var div = Element.Create("div"); div.Id = "lala"; x.AppendChild(div); container.AppendChild(x); var set = new HashSet(); foreach (var node in GetFlattened(container)) { if (node is Element child && !string.IsNullOrEmpty(child.Id)) { set.Add(child.Id); } } Assert.Contains("div1", set); Assert.Contains("div2", set); Assert.Contains("div1a", set); Assert.DoesNotContain("lala", set); } private static IEnumerable GetAllDescendents(Element element) { return RecursiveExtension(element, x => x.GetAllDescendants()); } private static IEnumerable GetFlattened(Element element) { return RecursiveExtension(element, x => x.GetLightChildren()); } private static IEnumerable RecursiveExtension(Element root, Func> method) { foreach (var node in method(root)) { yield return node; if (node is not Element child) continue; foreach (var grandchild in RecursiveExtension(child, method)) { yield return grandchild; } } } [Fact] [Obsolete("Old methods")] public void GetSlotElementFinds() { var x = new MySlotter(); var builder = new LaraBuilder(x); builder.Push("div", "", "slot1") .Pop() .Push("div", "", "slot2") .Push("div", "", "slot2a") .Pop() .Pop() .Push("div", "", "slot3") .Attribute("slot", "a") .Pop(); var set = new HashSet(); foreach (var item in x.GetSlottedElements("")) { if (item is Element element) { set.Add(element.Id); } } Assert.Contains("slot1", set); Assert.Contains("slot2", set); Assert.DoesNotContain("slot2a", set); Assert.DoesNotContain("slot3", set); var list = new List(x.GetSlottedElements("a")); Assert.Single(list); var child = list[0] as Element; Assert.NotNull(child); Assert.Equal("slot3", child!.Id); } [Fact] public void ComponentNotifiedAttributeChanged() { _app.PublishComponent(new WebComponentOptions { ComponentTagName = "x-att", ComponentType = typeof(MyAttributeSubscriptor) }); var x = new MyAttributeSubscriptor(); x.SetAttribute("data-lala", "lolo"); Assert.Equal("lolo", x.MyData); } private class MyAttributeSubscriptor : WebComponent { public MyAttributeSubscriptor() : base("x-att") { } public string? MyData { get; private set; } protected override IEnumerable GetObservedAttributes() { return new[] { "data-lala" }; } protected override void OnAttributeChanged(string attribute) { if (attribute == "data-lala") { MyData = GetAttribute("data-lala"); } } } [Fact] public void ObservedOnlyAttributeDoesNothing() { var x = new MyDummyComponent { Class = "lala" }; Assert.Equal("lala", x.Class); } [Fact] [Obsolete("Old methods")] public void PublishAssembliesComponent() { Assert.True(LaraUI.Context.Application.TryGetComponent("x-com", out var type)); Assert.Same(typeof(Xcom), type); } [Fact] [Obsolete("Old methods")] public void SlotsPrintHostElements() { var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var builder = new LaraBuilder(document.Body); builder.Push("x-slotter") .Push("div", "lalala") .AppendText("hello") .Pop() .Pop(); var writer = new DocumentWriter(document); writer.Print(); var html = writer.ToString(); Assert.Contains("lalala", html); Assert.Contains("hello", html); Assert.DoesNotContain("x-slotter", html); } [Fact] [Obsolete("Old methods")] public void OrphanSlotPrintsItself() { var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var builder = new LaraBuilder(document.Body); builder.Push("slot", "lalala").Pop(); var writer = new DocumentWriter(document); writer.Print(); var html = writer.ToString(); Assert.Contains("lalala", html); } [Fact] public void SlotNameSetsAttribute() { var x = new Slot { Name = "lala" }; Assert.Equal("lala", x.Name); Assert.Equal("lala", x.GetAttribute("name")); } [Fact] public void WebComponentsRequireDash() { var registry = new ComponentRegistry(); var found = false; try { registry.Register("baba", typeof(MyComponent)); } catch (ArgumentException) { found = true; } Assert.True(found); } [Fact] public void WebComponentsMustInherit() { var registry = new ComponentRegistry(); var found = false; try { registry.Register("x-lolo", typeof(HtmlInputElement)); } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Fact] public void CannotRegisterSameTagTwice() { var registry = new ComponentRegistry(); registry.Register("x-baba", typeof(MyComponent)); var found = false; try { registry.Register("x-baba", typeof(MyComponent)); } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Fact] public void VerifyComponentSameType() { Assert.False(WebComponent.VerifyType("x-com", typeof(MyComponent), out _)); } [Fact] public void NotifyMovedCalledDirectly() { var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var x = new MyDummyComponent(); var div1 = Element.Create("div"); var div2 = Element.Create("div"); document.Body.AppendChild(div1); document.Body.AppendChild(div2); div1.AppendChild(x); Assert.Equal(0, x.Moved); div2.AppendChild(x); Assert.Equal(1, x.Moved); } [Fact] public void GetContentNodeReturnsShadowChildren() { var component = new MyTwoDivComponent(true); var content = component.GetContentNode(); Assert.Equal(ContentNodeType.Array, content.Type); var array = content as ContentArrayNode; Assert.NotNull(array); Assert.NotNull(array!.Nodes); Assert.Equal(2, array.Nodes!.Count); Assert.Equal(ContentNodeType.Element, array.Nodes[0].Type); Assert.Equal(ContentNodeType.Text, array.Nodes[1].Type); } [Fact] [Obsolete("old method")] public void AttachShadowExecutes() { var x = new ObsoleteComponent(); x.Test(); Assert.Equal(1, x.Counter); } [Fact] public void ParentSlotNotSlotting() { var slot = new Slot { IsSlotted = true }; var x = Element.Create("div"); slot.AppendChild(x); Assert.False(SlottedCalculator.IsParentSlotting(x)); } [Fact] public void ShadowLightSlottedEmpty() { var component = new MyDummyComponent(); var div = Element.Create("div"); var x = component.GetShadow(); x.AppendChild(div); Assert.Empty(x.GetLightSlotted()); } [Fact] public void ShadowNotPrintable() { var div = Element.Create("div"); var component = new MyDummyComponent(); var x = component.GetShadow(); Assert.False(x.IsPrintable); Assert.True(div.IsPrintable); Assert.False(component.IsPrintable); } [Fact] public void SlotNameMatches() { var x = new Slot { Name = "red" }; Assert.True(x.MatchesName("red")); } [Fact] public void TriggerEventRuns() { var counter = 0; var x = new MyDummyComponent(); x.On("click", () => { counter++; return Task.CompletedTask; }); x.TriggerEvent("click"); x.TriggerEvent("lala"); Assert.Equal(1, counter); } [Fact] public void ContentPlaceholderClass() { var x = new ContentPlaceholder("test"); Assert.Equal("test", x.ElementId); } } } ================================================ FILE: src/Tests/DOM/AttributesTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using Microsoft.AspNetCore.Http; using Moq; using System; using System.Text; using Xunit; namespace Integrative.Lara.Tests.DOM { public class AttributesTesting : DummyContextTesting { [Fact] public void HasAttributeFindsAttribute() { var x = new Attributes(Element.Create("button")); x.SetAttributeLower("href", "lala"); Assert.True(x.HasAttribute("href")); Assert.False(x.HasAttribute("lala")); Assert.Equal("lala", x.GetAttribute("HREF")); } [Fact] public void ValueAttributeEnqueued() { var document = CreateDocument(); var element = Element.Create("button", "mybutton"); document.Body.AppendChild(element); document.OpenEventQueue(); element.SetAttribute("value", "5"); var queue = document.GetQueue(); Assert.NotEmpty(queue); var peek = queue.Peek(); Assert.True(peek is SetValueDelta); } private static Document CreateDocument() { var guid = Guid.Parse("{0857AE93-8591-4CB6-887E-C449ABFCAA7A}"); var page = new MyPage(); return new Document(page, guid, BaseModeController.DefaultKeepAliveInterval); } [Fact] public void SetFlagAttributeAddsNullValue() { var element = Element.Create("span"); element.Hidden = true; Assert.True(element.HasAttribute("hidden")); Assert.Equal("", element.GetAttribute("hidden")); var count = 0; foreach (var pair in element.Attributes) { if (pair.Key == "id") continue; count++; Assert.Equal("hidden", pair.Key); Assert.Equal("", pair.Value); } Assert.Equal(1, count); element.Hidden = false; Assert.False(element.HasAttribute("hidden")); } [Fact] public void GetNonExistingReturnsEmpty() { var element = Element.Create("button"); Assert.Equal(string.Empty, element.GetAttribute("lele")); } [Fact] public void RemovingAttributeRemovesValue() { var element = Element.Create("button"); element.SetAttribute("data-test", "lala"); element.RemoveAttribute("data-test"); Assert.Empty(element.GetAttribute("data-test")); } [Fact] public void ReplacingSameValueNoQueue() { var element = Element.Create("div"); element.Class = "lala"; var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); document.Body.AppendChild(element); document.OpenEventQueue(); element.Class = "lala"; Assert.Empty(document.GetQueue()); } [Fact] public void NotifySelectedSetsSelected() { var option = new HtmlOptionElement(); option.NotifyValue(new ElementEventValue { Checked = true, }); Assert.True(option.Selected); } [Fact] public void CreateNsSetsXlmns() { var x = Element.CreateNS("abc", "svg"); Assert.Equal("abc", x.GetAttribute("xlmns")); } [Fact] public void RemoveAttributeMissingSucceeds() { var document = CreateDocument(); var x = Element.Create("div"); document.Body.AppendChild(x); document.OpenEventQueue(); x.RemoveAttribute("lala"); Assert.Empty(document.GetQueue()); } [Fact] public void AttributesNotifyValueRemovesPrevious() { var x = Element.Create("div"); x.SetAttributeLower("value", "one"); x.NotifyValue("two"); Assert.Equal("two", x.GetAttribute("value")); } [Fact] public void MaxLevelDeep() { var found = false; try { DocumentWriter.VerifyNestedLevel(DocumentWriter.MaxLevelDeep + 1); } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Fact] public void ToggleClassFlipsClass() { Assert.Equal("lala", ClassEditor.ToggleClass("lala lolo", "lolo")); Assert.Equal("lala lolo", ClassEditor.ToggleClass("lala", "lolo")); } [Fact] public void ElementToggleClass() { var x = Element.Create("div"); x.Class = "lala lolo"; x.ToggleClass("lolo"); Assert.Equal("lala", x.Class); } [Fact] public void TagNameCannotHaveSpaces() { var found = false; try { Element.Create(" a"); } catch (ArgumentException) { found = true; } Assert.True(found); } [Fact] public void NotifyFlagSkipsSameValue() { var input = new HtmlInputElement { Checked = true }; input.NotifyChecked(true); Assert.True(input.Checked); } [Fact] public void NotifyValueSkipsSameValue() { var input = new HtmlInputElement { Value = "hello" }; input.NotifyValue("hello"); Assert.Equal("hello", input.Value); } [Fact] public void ModifySlotElementWithParent() { var x = Element.Create("div"); var div = Element.Create("div"); div.AppendChild(x); Assert.ThrowsAny(() => x.SetAttributeLower("slot", "test")); } [Fact] [Obsolete("Old methods")] public void SetInnerText() { var x = Element.Create("div"); x.SetInnerText("", x.InnerText); } [Fact] public void NodeInnerText() { var x = new TextNode(); Assert.Equal(string.Empty, x.InnerText); x.InnerText = "<<"; Assert.Equal("<<", x.InnerText); } [Fact] public void InputFilesAdd() { var file = new Mock(); file.Setup(x1 => x1.Name).Returns("abc"); var x = new HtmlInputElement { Type = "file" }; x.AddFile(file.Object); Assert.Single(x.Files); var found = x.Files[0]; Assert.Equal("abc", found.Name); } } } ================================================ FILE: src/Tests/DOM/BindingsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Xunit; namespace Integrative.Lara.Tests.DOM { internal class MyInputData : BindableBase { private string? _myvalue; public string? MyValue { get => _myvalue; set => SetProperty(ref _myvalue, value); } private bool _mychecked; public bool MyChecked { get => _mychecked; set => SetProperty(ref _mychecked, value); } } public class BindingsTesting : DummyContextTesting { [Fact] public void SetInnerTextSetsText() { var x = Element.Create("span"); x.InnerText = "hello"; Assert.Equal(1, x.ChildCount); VerifyInnerData(x, "hello"); } private static void VerifyInnerData(Element element, string data) { var node = element.GetChildAt(0) as TextNode; Assert.NotNull(node); Assert.Equal(data, node!.Data); } [Fact] public void InnerTextReplacesPrevious() { const string bye = " { BindObject = data, Property = x => x.Counter.ToString() }); data.Counter = 5; VerifyInnerData(div, "5"); } [Fact] [Obsolete("old methods")] public void BindGenericExecutes() { var data = new MyData(); var div = Element.Create("div"); div.Bind(new BindHandlerOptions { BindObject = data, ModifiedHandler = (_, _) => div.InnerText = data.Counter.ToString() }); data.Counter = 5; VerifyInnerData(div, "5"); } [Fact] [Obsolete("old methods")] public void BindActionExecutes() { var data = new MyData(); var div = Element.Create("div"); div.Bind(data, _ => div.InnerText = data.Counter.ToString()); data.Counter = 5; VerifyInnerData(div, "5"); } [Fact] [Obsolete("old methods")] public void BindAttributeExecutes() { var data = new MyData(); var div = Element.Create("div"); div.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = x => x.Counter.ToString() }); data.Counter = 5; Assert.Equal("5", div.GetAttribute("data-counter")); } [Fact] [Obsolete("old methods")] public void BindChildrenUpdates() { var collection = new ObservableCollection { new MyData() }; var span = Element.Create("span"); span.BindChildren(new BindChildrenOptions(collection, MyCreateCallback)); collection.Add(new MyData()); Assert.Equal(2, span.ChildCount); } [Fact] [Obsolete("old methods")] public void UnbindAllUnbinds() { var collection = new ObservableCollection(); var data = new MyData(); var div = Element.Create("div"); var span1 = Element.Create("span"); var span2 = Element.Create("span"); span2.Bind(data, _ => span2.InnerText = data.Counter.ToString()); div.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = x => x.Counter.ToString() }); div.BindChildren(new BindChildrenOptions(collection, _ => Element.Create("div"))); span1.BindInnerText(new BindInnerTextOptions { BindObject = data, Property = x => x.Counter.ToString() }); data.Counter = 5; div.UnbindAll(); span1.UnbindAll(); span2.UnbindAll(); collection.Add(data); data.Counter = 10; VerifyInnerData(span1, "5"); VerifyInnerData(span2, "5"); Assert.Equal(0, div.ChildCount); Assert.Equal("5", div.GetAttribute("data-counter")); } [Fact] [Obsolete("old methods")] public void UnbindAtributeRuns() { var x = Element.Create("div"); var data = new MyData(); x.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = y => y.Counter.ToString() }); data.Counter = 5; x.UnbindAll(); data.Counter = 10; Assert.Equal("5", x.GetAttribute("data-counter")); } [Fact] [Obsolete("old methods")] public void UnbindAttributeRemovesAllAttributes() { var x = Element.Create("div"); var data = new MyData(); x.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = y => y.Counter.ToString() }); x.BindAttribute(new BindAttributeOptions { Attribute = "data-counter2", BindObject = data, Property = y => y.Counter.ToString() }); data.Counter = 5; x.UnbindAll(); data.Counter = 10; Assert.Equal("5", x.GetAttribute("data-counter")); Assert.Equal("5", x.GetAttribute("data-counter2")); } [Fact] [Obsolete("old methods")] public void UnbindInnerTextWorks() { var div = Element.Create("div"); var data = new MyData { Counter = 5 }; div.BindInnerText(new BindInnerTextOptions { BindObject = data, Property = x => x.Counter.ToString() }); VerifyInnerData(div, "5"); div.UnbindAll(); data.Counter = 10; VerifyInnerData(div, "5"); } [Fact] [Obsolete("old methods")] public void UnbindHandlerWorks() { var div = Element.Create("div"); var data = new MyData(); div.Bind(data, _ => div.InnerText = data.Counter.ToString()); data.Counter = 3; div.UnbindAll(); data.Counter = 8; VerifyInnerData(div, "3"); } [Fact] [Obsolete("old methods")] public void UnbindChildrenWorks() { var collection = new ObservableCollection(); var div = Element.Create("div"); div.BindChildren(new BindChildrenOptions(collection, _ => Element.Create("span"))); collection.Add(new MyData()); div.UnbindAll(); collection.Clear(); Assert.NotEmpty(div.Children); } [Fact] [Obsolete("old methods")] public void GenericBindingDetectsCycles() { var div = Element.Create("div"); var data = new MyData(); div.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = x => x.Counter.ToString() }); div.Bind(new BindHandlerOptions { BindObject = data, ModifiedHandler = (_, _) => data.Counter++ }); var found = false; try { data.Counter = 3; } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Obsolete("old methods")] private static Element MyCreateCallback(MyData arg) { var span = Element.Create("span"); span.BindAttribute(new BindAttributeOptions { BindObject = arg, Attribute = "data-counter", Property = _ => arg.Counter.ToString() }); return span; } private class MyData : BindableBase { private int _counter; public MyData() { } public MyData(int counter) { Counter = counter; } public int Counter { get => _counter; set => SetProperty(ref _counter, value); } public bool IsEven => (_counter % 2) == 0; public override string ToString() => Counter.ToString(); } [Fact] public void BindableBaseSkipsUnncesaryEvents() { var raised = false; var data = new MyData(); data.PropertyChanged += (_, _) => raised = true; data.Counter = 0; Assert.False(raised); } [Fact] [Obsolete("old methods")] public void CollectionUpdaterMove() { var collection = new ObservableCollection(); var div = Element.Create("div"); div.BindChildren(new BindChildrenOptions(collection, MyCreateCallback)); collection.Add(new MyData(10)); collection.Add(new MyData(20)); collection.Add(new MyData(30)); collection.Add(new MyData(40)); collection.Add(new MyData(50)); VerifyPositions(collection, div); collection.Move(1, 2); VerifyPositions(collection, div); collection.RemoveAt(3); VerifyPositions(collection, div); collection[2] = new MyData(77); VerifyPositions(collection, div); collection.Clear(); VerifyPositions(collection, div); } private static void VerifyPositions(IReadOnlyList collection, Element div) { Assert.Equal(collection.Count, div.ChildCount); for (var index = 0; index < collection.Count; index++) { var data = collection[index]; VerifyPosition(div, index, data.Counter.ToString()); } } private static void VerifyPosition(Element div, int position, string value) { var child = (Element)div.GetChildAt(position); var current = child.GetAttribute("data-counter"); Assert.Equal(value, current); } [Fact] [Obsolete("old method")] public void BindFlagAttributeBinds() { var div = Element.Create("div"); var data = new MyData(); div.BindFlagAttribute(new BindFlagAttributeOptions { Attribute = "data-even", BindObject = data, Property = x => x.IsEven }); Assert.True(div.HasAttribute("data-even")); data.Counter++; Assert.False(div.HasAttribute("data-even")); } [Fact] [Obsolete("old methods")] public void BindToggleClassBinds() { var div = Element.Create("div"); var data = new MyData(); div.BindToggleClass(new BindToggleClassOptions { ClassName = "lala", BindObject = data, Property = x => x.IsEven }); Assert.True(div.HasClass("lala")); data.Counter++; Assert.False(div.HasClass("lala")); } [Fact] [Obsolete("old method")] public void LaraFlagBinding() { var div = Element.Create("div"); var builder = new LaraBuilder(div); var data = new MyData(); builder.BindFlagAttribute("data-even1", data, () => data.IsEven); builder.BindFlagAttribute("data-even2", data, x => x.IsEven); Assert.True(div.HasAttribute("data-even1")); Assert.True(div.HasAttribute("data-even2")); } [Fact] public void BindableBaseHoldsEvents() { var counter = 0; var data = new MyData(); data.PropertyChanged += (_, _) => counter++; data.BeginUpdate(); data.Counter = 5; Assert.Equal(0, counter); data.EndUpdate(); Assert.Equal(1, counter); } [Fact] [Obsolete("old methods")] public void InputBindingGetter() { var data = new MyInputData { MyValue = "hello" }; var input = new HtmlInputElement(); input.BindInput(new BindInputOptions { Attribute = "value", BindObject = data, Property = x => x.MyValue }); Assert.Equal("hello", input.Value); data.MyValue = "bye"; Assert.Equal("bye", input.Value); } [Fact] [Obsolete("old methods")] public void InputBindingGetterLara() { var data = new MyInputData { MyValue = "hello" }; var input = new HtmlInputElement(); var builder = new LaraBuilder(input); builder.BindInput("value", data, x => x.MyValue); Assert.Equal("hello", input.Value); data.MyValue = "bye"; Assert.Equal("bye", input.Value); } [Fact] [Obsolete("old methods")] public void InputBindingGetterLaraFlag() { var data = new MyInputData { MyChecked = true }; var input = new HtmlInputElement(); var builder = new LaraBuilder(input); builder.BindFlagInput("checked", data, x => x.MyChecked); Assert.True(input.Checked); data.MyChecked = false; Assert.False(input.Checked); } [Fact] [Obsolete("old methods")] public void InvalidSetterThrows() { var data = new MyInputData(); var x = new HtmlInputElement(); Assert.ThrowsAny(() => { x.BindInput(new BindInputOptions { Attribute = "value", BindObject = data, Property = x1 => x1.MyValue + "a" }); }); } [Fact] [Obsolete("old methods")] public void InputBindingCollects() { var input = new HtmlInputElement(); var data = new MyInputData(); input.BindInput(new BindInputOptions { BindObject = data, Attribute = "value", Property = x => x.MyValue }); input.Value = "hello"; Assert.Equal("hello", data.MyValue); } [Fact] [Obsolete("old methods")] public void InputBindingCollectsFlag() { var input = new HtmlInputElement(); var data = new MyInputData(); input.BindFlagInput(new BindFlagInputOptions { BindObject = data, Attribute = "checked", Property = x => x.MyChecked }); input.Checked = true; Assert.True(data.MyChecked); } [Fact] [Obsolete("old method")] public void LaraBindFlagAttribute() { var data = new MyInputData(); var div = Element.Create("div"); var builder = new LaraBuilder(div); builder.BindFlagAttribute(new BindFlagAttributeOptions { Attribute = "class", BindObject = data, Property = x => x.MyChecked }); data.MyChecked = true; Assert.True(div.HasAttribute("class")); } } } ================================================ FILE: src/Tests/DOM/BuilderTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 6/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.DOM { public class BuilderTesting : DummyContextTesting { [Fact] [Obsolete("Old methods")] public void PushAdds() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Push("button", "red", "mybutton").Pop(); Assert.NotEmpty(root.Children); var first = root.Children.FirstOrDefault() as HtmlButtonElement; Assert.NotNull(first); Assert.Equal("red", first!.Class); Assert.Equal("mybutton", first.Id); } [Fact] [Obsolete("Old methods")] public void TooManyPops() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Push("button", "red").Pop(); DomOperationsTesting.Throws(() => builder.Pop()); } [Fact] [Obsolete("Old methods")] public void AddSiblings() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Push("button", "red").Pop(); builder.Push("button", "red").Pop(); Assert.Equal(2, root.ChildCount); } [Fact] [Obsolete("Old methods")] public void AddTextNodeEncodes() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.AppendText("<"); var node = root.Children.FirstOrDefault() as TextNode; Assert.NotNull(node); Assert.Equal("&lt;", node!.Data); } [Fact] [Obsolete("Old methods")] public void AddElements() { var root = Element.Create("div"); var builder = new LaraBuilder(root); var list = new List() { new HtmlButtonElement(), new HtmlOptionElement() }; builder.AddNodes(list); Assert.Equal(2, root.ChildCount); } [Fact] [Obsolete("Old methods")] public void AddNodes() { var root = Element.Create("div"); var builder = new LaraBuilder(root); var list = new List() { new HtmlButtonElement(), new HtmlOptionElement() }; builder.AddNodes(list); Assert.Equal(2, root.ChildCount); } [Fact] [Obsolete("Old methods")] public void AddAction() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Add(MyAddAction); Assert.Equal(1, root.ChildCount); } [Obsolete("Old methods")] private static void MyAddAction(LaraBuilder builder) { builder.AddNode(new HtmlButtonElement()); } [Fact] [Obsolete("Old methods")] public void SetAttribute() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Attribute("class", "red"); Assert.Equal("red", root.Class); } [Fact] [Obsolete("Old methods")] public void SetFlag() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.FlagAttribute("hidden", true); Assert.True(root.Hidden); } [Fact] [Obsolete("Old methods")] public async void OnEvent() { var executed = false; var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.On(new EventSettings { EventName = "click", Handler = () => { executed = true; return Task.CompletedTask; } }); await root.NotifyEvent("click"); Assert.True(executed); } [Fact] [Obsolete("Old methods")] public async void OnEventSimple() { var executed = false; var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.On("click", () => { executed = true; return Task.CompletedTask; }); await root.NotifyEvent("click"); Assert.True(executed); } [Fact] [Obsolete("Old methods")] public void PushClassName() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.Push("div", "red").Pop(); Assert.NotEmpty(root.Children); var child = root.Children.FirstOrDefault() as Element; Assert.NotNull(child); Assert.Equal("red", child!.Class); } [Fact] [Obsolete("Old methods")] // ReSharper disable once InconsistentNaming public void PushNS() { var root = Element.Create("div"); var builder = new LaraBuilder(root); builder.PushNS("abc", "svg").Pop(); Assert.NotEmpty(root.Children); var child = root.Children.FirstOrDefault() as Element; Assert.NotNull(child); Assert.Equal("abc", child!.GetAttribute("xlmns")); } [Fact] [Obsolete("Old methods")] public void PushElementClass() { var root = Element.Create("root"); var div = Element.Create("div"); var builder = new LaraBuilder(root); builder.Push(div, "red"); Assert.Equal("red", div.Class); } [Fact] public void SessionIdAvailable() { var guid = Guid.Parse("{0F9EE9CD-F9A0-40E6-A91B-FE4E3E2282F0}"); var cnx = new Connection(guid, IPAddress.Loopback); var x = new Session(cnx); Assert.Equal(guid, x.SessionId); } } } ================================================ FILE: src/Tests/DOM/ClassEditorTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Xunit; namespace Integrative.Lara.Tests.DOM { public class ClassEditorTesting { [Fact] public void HasEmptyClassTrue() { Assert.True(ClassEditor.HasClass("", "")); } [Fact] public void EmptyClassFalse() { Assert.False(ClassEditor.HasClass("", "lele")); } [Fact] public void HasClassTrue() { Assert.True(ClassEditor.HasClass("aaa", "aaa")); Assert.True(ClassEditor.HasClass("aaa b", "aaa")); Assert.True(ClassEditor.HasClass("b aaa", "aaa")); Assert.True(ClassEditor.HasClass(" aaa", "aaa")); Assert.True(ClassEditor.HasClass("cc aaa ddd", "aaa")); } [Fact] public void RemoveClass() { Assert.Equal("lala", ClassEditor.RemoveClass("lala", "")); Assert.Equal("", ClassEditor.RemoveClass("lala", "lala")); Assert.Equal("", ClassEditor.RemoveClass(" lala ", "lala")); Assert.Equal("", ClassEditor.RemoveClass("lala", " lala ")); Assert.Equal("blue", ClassEditor.RemoveClass("lala blue", "lala")); Assert.Equal("blue", ClassEditor.RemoveClass("blue lala", "lala")); Assert.Equal("red blue", ClassEditor.RemoveClass("red lala blue", "lala")); Assert.Equal("orange", ClassEditor.RemoveClass("orange", "lala")); } [Fact] public void AddClass() { Assert.Equal("lala", ClassEditor.AddClass("lala", "lala")); Assert.Equal("lala", ClassEditor.AddClass(" ", "lala")); Assert.Equal(" red lala", ClassEditor.AddClass(" red", "lala")); } } } ================================================ FILE: src/Tests/DOM/DomOperationsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.DOM { public class DomOperationsTesting : DummyContextTesting { private readonly Func _emptyHandler; public DomOperationsTesting() { _emptyHandler = (() => Task.CompletedTask); } [Fact] public void AddElementWithId() { var button = Element.Create("button", "mybutton"); var document = CreateDocument(); document.Body.AppendChild(button); Assert.True(document.TryGetElementById("mybutton", out var found)); Assert.Same(button, found); } internal static Document CreateDocument() { var guid = Connections.CreateCryptographicallySecureGuid(); var page = new MyPage(); return new Document(page, guid, BaseModeController.DefaultKeepAliveInterval); } [Fact] public void AddBranchWithId() { var button = Element.Create("button", "mybutton"); var span = Element.Create("span", "myspan"); button.AppendChild(span); var document = CreateDocument(); document.Body.AppendChild(button); Assert.True(document.TryGetElementById("myspan", out var found)); Assert.Same(span, found); } [Fact] public void RemoveElementWithId() { var button = Element.Create("button", "mybutton"); var document = CreateDocument(); document.Body.AppendChild(button); button.Remove(); Assert.False(document.TryGetElementById("mybutton", out _)); } [Fact] public void RemoveBranchWithIdInside() { var button = Element.Create("button"); var span = Element.Create("span", "myspan"); button.AppendChild(span); var doc = CreateDocument(); doc.Body.AppendChild(button); button.Remove(); Assert.False(doc.TryGetElementById("myspan", out _)); } [Fact] public void CannotRemoveDocumentHead() { var doc = CreateDocument(); Assert.Throws(() => doc.Head.Remove()); } [Fact] public void CannotRemoveDocumentBody() { var doc = CreateDocument(); Assert.Throws(() => doc.Body.Remove()); } [Fact] public void CannotAddDuplicateId() { var button1 = Element.Create("button", "mybutton"); var button2 = Element.Create("button", "mybutton"); var div = Element.Create("div"); div.AppendChild(button1); div.AppendChild(button2); var doc = CreateDocument(); Throws(() => doc.Body.AppendChild(div)); } [Fact] public void CannotInsertDuplicateId() { var button1 = Element.Create("button", "mybutton"); var button2 = Element.Create("button", "mybutton"); var div = Element.Create("div"); div.AppendChild(button1); div.AppendChild(button2); var doc = CreateDocument(); var pane = Element.Create("div"); doc.Body.AppendChild(pane); Throws(() => doc.Body.InsertChildAfter(pane, div)); } [Fact] public void CannotAddNodeInsideItself() { var e1 = Element.Create("span"); var e2 = Element.Create("span"); e1.AppendChild(e2); Throws(() => e2.AppendChild(e1)); } internal static void Throws(Action action) where T : Exception { var error = false; try { action(); } catch (T) { error = true; } Assert.True(error); } internal static async Task ThrowsAsync(Func action) where T : Exception { var error = false; try { await action(); } catch (T) { error = true; } Assert.True(error); } [Fact] public void TextNodeContent() { var node = new TextNode("hello"); var x = node.GetContentNode(); Assert.Equal(NodeType.Text, node.NodeType); Assert.Equal(ContentNodeType.Text, x.Type); Assert.True(x is ContentTextNode); Assert.Equal("hello", ((ContentTextNode)x).Data); } [Fact] public void InsertBeforeInserts() { var div = Element.Create("div"); var span1 = Element.Create("span"); var span2 = Element.Create("span"); div.AppendChild(span2); div.InsertChildBefore(span2, span1); var list = new List(div.Children); Assert.NotEmpty(list); Assert.Equal(2, list.Count); Assert.Equal(2, div.ChildCount); Assert.Same(span1, list[0]); Assert.Same(span2, list[1]); } [Fact] public void GenerateIdsForEvents() { var span = Element.Create("span"); span.On("click", _emptyHandler); var div = Element.Create("div"); div.On("click", _emptyHandler); div.AppendChild(span); var doc = CreateDocument(); doc.Body.AppendChild(div); Assert.False(string.IsNullOrEmpty(span.Id)); Assert.False(string.IsNullOrEmpty(div.Id)); } [Fact] public void GenerateIdsForEventsInsert() { var span = Element.Create("span"); span.On("click", _emptyHandler); var div = Element.Create("div"); div.On("click", _emptyHandler); div.AppendChild(span); var dummy = Element.Create("button"); var doc = CreateDocument(); doc.Body.AppendChild(dummy); doc.Body.InsertChildBefore(dummy, div); Assert.False(string.IsNullOrEmpty(span.Id)); Assert.False(string.IsNullOrEmpty(div.Id)); } [Fact] public void TransferElementBetweenDocuments() { var button = Element.Create("button", "mybutton"); var doc1 = CreateDocument(); var doc2 = CreateDocument(); doc1.Body.AppendChild(button); doc2.Body.AppendChild(button); Assert.False(doc1.TryGetElementById("mybutton", out _)); Assert.True(doc2.TryGetElementById("mybutton", out var found)); Assert.Same(button, found); } [Fact] public void RemoveTextNode() { var div = Element.Create("div", "mydiv"); var doc = CreateDocument(); doc.Body.AppendChild(new TextNode("hi")); doc.Body.AppendChild(div); doc.Body.AppendChild(new TextNode("bye")); doc.OpenEventQueue(); doc.Body.RemoveAt(2); var queue = doc.GetQueue(); Assert.NotEmpty(queue); var step = queue.Peek() as NodeRemovedDelta; Assert.NotNull(step); Assert.Equal(doc.Body.Id, step!.ParentId); Assert.Equal(2, step.ChildIndex); } [Fact] public void RemoveElement() { var div = Element.Create("div", "mydiv"); var doc = CreateDocument(); doc.Body.AppendChild(new TextNode("hi")); doc.Body.AppendChild(div); doc.Body.AppendChild(new TextNode("bye")); doc.OpenEventQueue(); div.Remove(); var queue = doc.GetQueue(); Assert.NotEmpty(queue); var step = queue.Peek() as RemoveElementDelta; Assert.NotNull(step); Assert.Equal(div.Id, step?.ElementId); } [Fact] public void NodeAdded() { var div = Element.Create("div", "mydiv"); var doc = CreateDocument(); doc.OpenEventQueue(); doc.Body.AppendChild(div); var queue = doc.GetQueue(); Assert.NotEmpty(queue); var step = queue.Peek() as NodeAddedDelta; Assert.NotNull(step); Assert.Equal(doc.Body.Id, step!.ParentId); var content = step.Node as ContentElementNode; Assert.NotNull(content); Assert.Equal("div", content!.TagName); Assert.NotNull(content.Attributes?.FirstOrDefault()); var att = content.Attributes![0]; Assert.Equal("id", att.Attribute); Assert.Equal("mydiv", att.Value); } [Fact] public void NodeInsertedDelta() { var div = Element.Create("div", "mydiv"); var doc = CreateDocument(); var text = new TextNode("lala"); doc.Body.AppendChild(text); doc.OpenEventQueue(); doc.Body.InsertChildAfter(text, div); var queue = doc.GetQueue(); Assert.NotEmpty(queue); var step = queue.Peek() as NodeInsertedDelta; Assert.NotNull(step); Assert.Equal(doc.Body.Id, step!.ParentElementId); Assert.Equal(1, step.Index); var content = step.Node as ContentElementNode; Assert.NotNull(content); Assert.Equal("div", content!.TagName); Assert.NotNull(content.Attributes?.FirstOrDefault()); var att = content.Attributes![0]; Assert.Equal("id", att.Attribute); Assert.Equal("mydiv", att.Value); } [Fact] public void FocusFailsOnGet() { var div = Element.Create("div"); Throws(() => div.Focus()); } [Fact] public void RemoveOrphanThrows() { var div = Element.Create("div"); Throws(() => div.Remove()); } [Fact] public void InsertBeforeUnknownThrows() { var div1 = Element.Create("div"); var div2 = Element.Create("div"); var div3 = Element.Create("div"); Throws(() => div1.InsertChildBefore(div2, div3)); } [Fact] public void RemoveUnknownChildThrows() { var a = Element.Create("span"); var b = Element.Create("span"); Throws(() => a.RemoveChild(b)); } [Fact] public void ClearChildrenRemovesThem() { var div = Element.Create("div"); var span1 = Element.Create("span"); var span2 = Element.Create("span"); div.AppendChild(span1); div.AppendChild(span2); div.ClearChildren(); Assert.Empty(div.Children); Assert.Equal(0, div.ChildCount); } [Fact] public void InsertAtSucceeds() { var x = Element.Create("div"); var a = Element.Create("div"); var b = Element.Create("div"); var c = Element.Create("div"); x.AppendChild(a); x.AppendChild(c); x.InsertChildAt(1, b); Assert.Equal(3, x.ChildCount); Assert.Same(b, x.GetChildAt(1)); } [Fact] public void RemoveAtSucceeds() { var x = Element.Create("div"); var a = Element.Create("div"); var b = Element.Create("div"); x.AppendChild(a); x.AppendChild(b); x.RemoveAt(1); Assert.Equal(1, x.ChildCount); Assert.Same(a, x.GetChildAt(0)); } [Fact] public void MissingEventNameThrows() { var settings = new EventSettings(); var found = false; try { settings.Verify(); } catch (ArgumentException) { found = true; } Assert.True(found); } [Fact] public void DocumentGetElementById() { var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var div = Element.Create("div"); div.Id = "lala"; document.Body.AppendChild(div); var found = document.GetElementById("lala"); Assert.Same(div, found); } [Fact] public async void DocumentOnUnloadExecutes() { var counter = 0; var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); document.OnUnload += (_, _) => counter++; await document.NotifyUnload(); Assert.Equal(1, counter); } [Fact] public void SwapChildrenSwaps() { var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var div = Element.Create("div"); var n1 = new TextNode("n1"); var n2 = new TextNode("n2"); var n3 = new TextNode("n3"); div.AppendChild(n1); div.AppendChild(n2); div.AppendChild(n3); document.Body.AppendChild(div); document.OpenEventQueue(); div.SwapChildren(1, 1); div.SwapChildren(0, 2); Assert.Equal(3, div.ChildCount); Assert.Same(n3, div.GetChildAt(0)); Assert.Same(n1, div.GetChildAt(2)); var queue = document.GetQueue(); Assert.NotEmpty(queue); var first = queue.Peek() as SwapChildrenDelta; Assert.NotNull(first); Assert.Equal(div.Id, first!.ParentId); Assert.NotNull(div.Id); Assert.Equal(0, first.Index1); Assert.Equal(2, first.Index2); } [Fact] public void InputNotifyValueUpdates() { var input = new HtmlInputElement(); input.NotifyValue(new ElementEventValue { Checked = true, ElementId = input.Id, Value = "a" }); Assert.True(input.Checked); Assert.Equal("a", input.Value); } [Fact] // ReSharper disable once InconsistentNaming public void ElementGetChildPosition2nd() { var div = Element.Create("div"); var x1 = Element.Create("div"); var x2 = Element.Create("div"); div.AppendChild(x1); div.AppendChild(x2); var index = div.GetChildElementPosition(x2); Assert.Equal(1, index); } [Fact] public void RemoveEventRemovesIt() { var div = Element.Create("div"); div.On("click", () => Task.CompletedTask); div.On("click", null); Assert.Empty(div.Events); } [Fact] public void ElementAppendDataWorks() { var div = Element.Create("div"); div.AppendData("@@"); Assert.Equal(1, div.ChildCount); var child = div.GetChildAt(0) as TextNode; Assert.NotNull(child); Assert.Equal("@@", child!.Data); } private class DummyAdoptable : WebComponent { public int AdoptedCount { get; private set; } public DummyAdoptable() : base("x-adoptable") { } protected override void OnAdopted() { AdoptedCount++; } } [Fact] public void NotifyAdoptedPassedToChildren() { Context.Application.PublishComponent(new WebComponentOptions { ComponentTagName = "x-adoptable", ComponentType = typeof(DummyAdoptable) }); var div = Element.Create("div"); var x = new DummyAdoptable(); div.AppendChild(x); var doc1 = CreateDocument(); doc1.Body.AppendChild(div); var doc2 = CreateDocument(); doc2.Body.AppendChild(div); Assert.Equal(1, x.AdoptedCount); Context.Application.UnPublishWebComponent("x-adoptable"); } [Fact] public void SetInnerDataSetsData() { var div = Element.Create("div"); div.SetInnerData("@@"); Assert.Equal(1, div.ChildCount); var child = div.GetChildAt(0) as TextNode; Assert.NotNull(child); Assert.Equal("@@", child!.Data); } [Fact] public void SetInnerTextReplacesText() { var div = Element.Create("div"); div.InnerText = "bb"; div.InnerText = "a
    ", html); } [Fact] public void FocusEnqueues() { var doc = CreateDocument(); var div = Element.Create("div"); doc.Body.AppendChild(div); div.Focus(); var q = doc.GetQueue(); Assert.Single(q); var first = q.Peek() as FocusDelta; Assert.NotNull(first); Assert.Equal(div.Id, first!.ElementId); } [Fact] public void ButtonNotifyValue() { var x = new HtmlButtonElement(); x.NotifyValue(new ElementEventValue { ElementId = x.Id, Value = "test" }); Assert.Equal("test", x.GetAttribute("value")); } [Fact] public void TextNodeAppendData() { var x = new TextNode(); x.AppendData("a("a"); TestElement("button"); TestElement("colgroup"); TestElement("img"); TestElement("input"); TestElement("label"); TestElement("link"); TestElement("li"); TestElement("meta"); TestElement("meter"); TestElement("option"); TestElement("optgroup"); TestElement("ol"); TestElement("script"); TestElement("select"); TestElement("table"); TestElement("td"); TestElement("th"); TestElement("textarea"); Assert.NotNull(Document.CreateElement("tbody") as HtmlTableSectionElement); Assert.NotNull(Document.CreateElement("tfoot") as HtmlTableSectionElement); Assert.NotNull(Document.CreateElement("thead") as HtmlTableSectionElement); } [Fact] public void ElementNeedsTag() { DomOperationsTesting.Throws(() => Element.Create("")); } private void TestElement(string tagName) where T : Element { var instance = Activator.CreateInstance(typeof(T)) as Element; Assert.NotNull(instance); Assert.Equal(tagName, instance!.TagName); TestProperties(instance); } private void TestProperties(Element instance) { var type = instance.GetType(); foreach (var property in type.GetProperties()) { if (property.SetMethod != null) { TestProperty(instance, property); } } } private void TestProperty(Element instance, PropertyInfo property) { var type = property.PropertyType; if (!GetTestValue(type, out var value)) { return; } property.SetValue(instance, value); var result = property.GetValue(instance); Assert.Equal(value, result); } private bool GetTestValue(Type type, [NotNullWhen(true)] out object? value) { _counter++; if (type == typeof(bool)) { value = true; } else if (type == typeof(int)) { value = _counter; } else if (type == typeof(string)) { value = _counter.ToString(); } else { value = null; return false; } return true; } [Fact] public void GetChildPositionNotFound() { var a = Element.Create("div"); var b = Element.Create("div"); var index = b.GetChildElementPosition(a); Assert.Equal(-1, index); } [Fact] public void ElementDescendsFromItself() { var x = Element.Create("div"); Assert.True(x.DescendsFrom(x)); } [Fact] public void SetIntAttribute() { var input = new HtmlInputElement { Height = 10 }; Assert.Equal(10, input.Height); input.Height = null; Assert.Null(input.Height); } [Fact] public async void ElementOnOptions() { var executed = false; var div = Element.Create("div"); div.On(new EventSettings { EventName = "click", Handler = () => { executed = true; return Task.CompletedTask; } }); var context = new Mock(); LaraUI.InternalContext.Value = context.Object; await div.NotifyEvent("click"); Assert.True(executed); } [Fact] public void RemoveClassRemovesClass() { var button = new HtmlButtonElement { Class = "red blue green" }; button.RemoveClass("blue"); Assert.Equal("red green", button.Class); } [Fact] public void AddClassAddsClass() { var button = new HtmlButtonElement(); button.AddClass("red"); Assert.Equal("red", button.Class); } [Fact] public void SetFlagAttributes() { var button = new HtmlButtonElement(); button.SetFlagAttribute("hidden", true); Assert.True(button.Hidden); } [Fact] public void InputAttributes() { var input = new HtmlInputElement { MaxLength = 5, Size = 3, Width = 11 }; Assert.Equal(5, input.MaxLength); Assert.Equal(3, input.Size); Assert.Equal(11, input.Width); } [Fact] public void EncodeTextNode() { var n1 = new TextNode("<"); var n2 = new TextNode("<", false); Assert.Equal("&lt;", n1.Data); Assert.Equal("<", n2.Data); } [Fact] public void ImageProperties() { var image = new HtmlImageElement { Height = "1", Width = "2" }; Assert.Equal("1", image.Height); Assert.Equal("2", image.Width); } [Fact] public void OrderedListAttributes() { var ol = new HtmlOlElement { Start = 1 }; Assert.Equal(1, ol.Start); } [Fact] public void TextAreaProperties() { var x = new HtmlTextAreaElement { Cols = 1, MaxLength = 2, Rows = 3 }; Assert.Equal(1, x.Cols); Assert.Equal(2, x.MaxLength); Assert.Equal(3, x.Rows); } [Fact] public void NotifyValueTextArea() { var x = new HtmlTextAreaElement(); x.NotifyValue(new ElementEventValue { Value = "lala" }); Assert.Equal("lala", x.Value); } [Fact] public void TableHeaderProperties() { var x = new HtmlTableHeaderElement { ColSpan = 1, RowSpan = 2 }; Assert.Equal(1, x.ColSpan); Assert.Equal(2, x.RowSpan); } [Fact] public void EventSettingsAttributes() { var x = new EventSettings { LongRunning = true, BlockOptions = new BlockOptions { BlockedElementId = "aaa", ShowHtmlMessage = "baa", ShowElementId = "xxx" } }; Assert.True(x.Block); Assert.Equal("aaa", x.BlockOptions.BlockedElementId); Assert.Equal("baa", x.BlockOptions.ShowHtmlMessage); Assert.Equal("xxx", x.BlockOptions.ShowElementId); Assert.True(x.LongRunning); } [Fact] public void LaraOptionsProperties() { var x = new LaraOptions { AllowLocalhostOnly = true, ShowNotFoundPage = false, AddWebSocketsMiddleware = false, Mode = ApplicationMode.BrowserApp, PublishAssembliesOnStart = true }; Assert.True(x.AllowLocalhostOnly); Assert.False(x.ShowNotFoundPage); Assert.False(x.AddWebSocketsMiddleware); Assert.Equal(ApplicationMode.BrowserApp, x.Mode); Assert.True(x.PublishAssembliesOnStart); } [Fact] public void DuplicateElementEmptyConstructor() { var instance = Activator.CreateInstance(); Assert.NotNull(instance); } [Fact] public void DuplicateElementInner() { var inner = new InvalidOperationException("lala"); var instance = new DuplicateElementIdException("lele", inner); Assert.Same(inner, instance.InnerException); Assert.Equal("lele", instance.Message); } [Fact] public void TableCellProperties() { var x = new HtmlTableCellElement { ColSpan = 1, RowSpan = 2 }; Assert.Equal(1, x.ColSpan); Assert.Equal(2, x.RowSpan); } [Fact] public void ColGroupProperties() { var x = new HtmlColGroupElement { Span = 1 }; Assert.Equal(1, x.Span); } [Fact] public void LoopSelectOptions() { var select = new HtmlSelectElement(); var option1 = new HtmlOptionElement(); var group = Element.Create("optgroup"); var option2 = new HtmlOptionElement(); group.AppendChild(option2); select.AppendChild(option1); select.AppendChild(group); Assert.Equal( new List{ option1, option2 }, select.Options); } [Fact] public void SelectProperties() { var select = new HtmlSelectElement { Size = 3 }; Assert.Equal(3, select.Size); } [Fact] public void SelectNotifyValue() { var select = new HtmlSelectElement(); select.NotifyValue(new ElementEventValue { Value = "lala" }); Assert.Equal("lala", select.Value); } [Fact] public void SelectAddOption() { var x = new HtmlSelectElement(); x.AddOption("myvalue", "this is the text"); var option = x.Options.FirstOrDefault(); Assert.NotNull(option); Assert.Equal("myvalue", option?.Value); var text = option?.Children.FirstOrDefault() as TextNode; Assert.NotNull(text); Assert.Equal("this is the text", text!.Data); } [Fact] public void OptionWithValueGetsSelected() { var select = new HtmlSelectElement { Value = "lolo" }; var option = new HtmlOptionElement { Value = "lolo" }; select.AppendChild(option); Assert.True(option.Selected); } [Fact] public void AddGroupWithSelectedOption() { var select = new HtmlSelectElement { Value = "lolo" }; var option = new HtmlOptionElement { Value = "lolo" }; var group = new HtmlOptionGroupElement(); group.AppendChild(option); select.AppendChild(group); Assert.True(option.Selected); } [Fact] public void AddSelectedOptionInGroup() { var select = new HtmlSelectElement { Value = "lolo" }; var option = new HtmlOptionElement { Value = "lolo" }; var group = new HtmlOptionGroupElement(); select.AppendChild(group); group.AppendChild(option); Assert.True(option.Selected); } [Fact] public void SelectValueChangeOnChildOptions() { var select = new HtmlSelectElement(); var opt1 = new HtmlOptionElement { Value = "a" }; var opt2 = new HtmlOptionElement { Value = "b" }; var group = new HtmlOptionGroupElement(); group.AppendChild(opt2); select.AppendChild(opt1); select.AppendChild(group); select.Value = "a"; Assert.True(opt1.Selected); Assert.False(opt2.Selected); select.Multiple = true; select.Value = "b"; Assert.True(opt1.Selected); Assert.True(opt2.Selected); } [Fact] public void MeterProperties() { var x = new HtmlMeterElement { High = 80, Low = 20, Max = 100, Min = 1, Optimum = 50, Value = 55 }; Assert.Equal(80, x.High); Assert.Equal(20, x.Low); Assert.Equal(100, x.Max); Assert.Equal(1, x.Min); Assert.Equal(50, x.Optimum); Assert.Equal(55, x.Value); } [Fact] public void ElementToStringSuffix() { var div = Element.Create("div"); div.Id = "lolo"; div.Class = "red"; Assert.Equal("div #lolo red", div.ToString()); } [Fact] public void IgnoreNotificationsNotFound() { var x = Element.Create("div"); var task = x.NotifyEvent("lala"); Assert.Same(Task.CompletedTask, task); } [Fact] public void AppendTextMergesNodes() { var x = Element.Create("div"); x.AppendText("hi"); x.AppendText(" "); x.AppendText("bye"); Assert.Equal(1, x.ChildCount); var node = x.GetChildAt(0) as TextNode; Assert.NotNull(node); Assert.Equal("hi bye", node!.Data); } } } ================================================ FILE: src/Tests/DOM/EventsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using Moq; using System; using System.Globalization; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.DOM { public class EventsTesting : DummyContextTesting { [Fact] public async void AddRemoveHandler() { var x = new MessageTypeRegistry(); var counter = 0; Task Handler(MessageEventArgs args1) { Assert.Equal("test", args1.Body); counter++; return Task.CompletedTask; } x.Add(Handler); var args = new MessageEventArgs("test"); await x.RunAll(args); Assert.Equal(1, counter); x.Remove(Handler); await x.RunAll(args); Assert.Equal(1, counter); } [Fact] public async void AddRemoveHandlerRegistry() { CreateMessageContext(); var document = DomOperationsTesting.CreateDocument(); var x = new MessageRegistry(document); var counter = 0; Task Handler(MessageEventArgs args) { counter++; return Task.CompletedTask; } x.Add("a", Handler); await document.Head.NotifyEvent("_a"); Assert.Equal(1, counter); x.Remove("b", Handler); await document.Head.NotifyEvent("_a"); Assert.Equal(2, counter); x.Remove("a", Handler); await document.Head.NotifyEvent("_a"); Assert.Equal(2, counter); } private void CreateMessageContext() { var context = new Mock(); LaraUI.InternalContext.Value = context.Object; var bridge = new Mock(); context.Setup(x => x.JSBridge).Returns(bridge.Object); bridge.Setup(x => x.EventData).Returns("test"); context.Setup(x => x.Application).Returns(Context.Application); } [Fact] public void DebounceStored() { var x = new EventSettings { DebounceInterval = 5 }; Assert.Equal(5, x.DebounceInterval); } [Fact] public void DocumentProcessesMessageListeners() { CreateMessageContext(); var doc = DomOperationsTesting.CreateDocument(); var counter = 0; Task Handler(MessageEventArgs args) { counter++; return Task.CompletedTask; } doc.AddMessageListener("a", Handler); doc.Head.NotifyEvent("_a"); Assert.Equal(1, counter); doc.RemoveMessageListener("a", Handler); doc.Head.NotifyEvent("_a"); Assert.Equal(1, counter); } [Fact] [Obsolete("old methods")] public void DocumentOnMessageRuns() { CreateMessageContext(); var doc = DomOperationsTesting.CreateDocument(); var counter = 0; Task Handler() { counter++; return Task.CompletedTask; } doc.OnMessage("a", Handler); doc.Head.NotifyEvent("_a"); Assert.Equal(1, counter); } [Fact] public async void AsyncEventDispatches() { var counter = 0; var ev = new AsyncEvent(); Task MyMethod() { counter++; return Task.CompletedTask; } ev.Subscribe(MyMethod); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); ev.Unsubscribe(MyMethod); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); } [Fact] public async void AsyncEventPassesAlong() { var counter = 0; var ev = new AsyncEvent(); ev.Subscribe(() => { counter++; return Task.CompletedTask; }); var ev2 = new AsyncEvent(); ev2.Subscribe(ev); await ev2.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); ev2.Unsubscribe(ev); await ev2.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); } [Fact] public async void AsyncEventSubscribeHandler() { var counter = 0; Task MyMethod(object sender, EventArgs args) { counter++; return Task.CompletedTask; } var handler = new AsyncEventHandler(MyMethod); var ev = new AsyncEvent(); ev.Subscribe(handler); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); ev.Unsubscribe(handler); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); } [Fact] public async void AsyncEventSyncHandler() { var counter = 0; void MyMethod() => counter++; var ev = new AsyncEvent(); ev.Subscribe(MyMethod); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); ev.Unsubscribe(MyMethod); await ev.InvokeAsync(this, new EventArgs()); Assert.Equal(1, counter); } [Fact] public void DocumentEvent() { var counter = 0; var x = new Document(new MyPage(), 100); x.On("keyup", () => { counter++; return Task.CompletedTask; }); x.NotifyEvent("keyup"); Assert.Equal(1, counter); x.NotifyEvent("keyup"); Assert.Equal(2, counter); x.On("keyup", null); x.NotifyEvent("keyup"); Assert.Equal(2, counter); } [Fact] public void DocumentGuidToString() { var x = new Document(new MyPage(), 100); var text = x.VirtualId.ToString(GlobalConstants.GuidFormat, CultureInfo.InvariantCulture); Assert.Equal(text, x.VirtualIdString); } } } ================================================ FILE: src/Tests/DOM/GlobalAttributesTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using System.Collections; using System.Collections.Generic; using Xunit; namespace Integrative.Lara.Tests.DOM { public class GlobalAttributesTesting : DummyContextTesting { [Fact] public void AccessKey() { var x = new HtmlButtonElement { AccessKey = "1", Class = "2", ContentEditable = "3", Dir = "4", Draggable = "5", DropZone = "6", Hidden = true, Id = "7", Lang = "8", Spellcheck = "9", Style = "10", TabIndex = "11", Title = "12", Translate = "13" }; Assert.Equal("1", x.GetAttribute("accesskey")); Assert.Equal("2", x.GetAttribute("class")); Assert.Equal("3", x.GetAttribute("contenteditable")); Assert.Equal("4", x.GetAttribute("dir")); Assert.Equal("5", x.GetAttribute("draggable")); Assert.Equal("6", x.GetAttribute("dropzone")); Assert.Equal("7", x.GetAttribute("id")); Assert.Equal("8", x.GetAttribute("lang")); Assert.Equal("9", x.GetAttribute("spellcheck")); Assert.Equal("10", x.GetAttribute("style")); Assert.Equal("11", x.GetAttribute("tabindex")); Assert.Equal("12", x.GetAttribute("title")); Assert.Equal("13", x.GetAttribute("translate")); Assert.True(x.Hidden); Assert.Equal("1", x.AccessKey); Assert.Equal("2", x.Class); Assert.Equal("3", x.ContentEditable); Assert.Equal("4", x.Dir); Assert.Equal("5", x.Draggable); Assert.Equal("6", x.DropZone); Assert.Equal("7", x.Id); Assert.Equal("8", x.Lang); Assert.Equal("9", x.Spellcheck); Assert.Equal("10", x.Style); Assert.Equal("11", x.TabIndex); Assert.Equal("12", x.Title); Assert.Equal("13", x.Translate); } [Fact] public void ElementToString() { var x = new HtmlButtonElement(); Assert.Equal($"button #{x.Id}", x.ToString()); x.Id = "hi"; Assert.Equal("button #hi", x.ToString()); } [Fact] public void SetAttributeId() { var x = Element.Create("button"); x.SetAttribute("ID", "x"); Assert.Equal("x", x.Id); } [Fact] public void GetChildPositionNotFound() { var x1 = Element.Create("span"); var x2 = Element.Create("span"); var index = x1.GetChildNodePosition(x2); Assert.Equal(-1, index); } [Fact] public void GrandchildDescendsFromElement() { var x1 = Element.Create("span"); var x2 = Element.Create("span"); var x3 = Element.Create("span"); x1.AppendChild(x2); x2.AppendChild(x3); Assert.True(x3.DescendsFrom(x1)); } [Fact] public void InsertChildAfter() { var div = Element.Create("div"); var x1 = Element.Create("span"); var x2 = Element.Create("span"); var x3 = Element.Create("span"); div.AppendChild(x1); div.AppendChild(x3); div.InsertChildAfter(x1, x2); var list = new List(div.Children); Assert.Equal(3, list.Count); Assert.Same(x2, list[1]); } [Fact] public void GetContentNodeElement() { var div = Element.Create("div", "mydiv"); var span = Element.Create("span"); var text = Element.Create("hello"); div.AppendChild(span); span.AppendChild(text); var content = div.GetContentNode(); Assert.True(content is ContentElementNode); var ce = (ContentElementNode)content; Assert.Equal("div", ce.TagName); Assert.Single(ce.Children); Assert.Single(ce.Attributes); var att = ce.Attributes![0]; Assert.Equal("id", att.Attribute); Assert.Equal("mydiv", att.Value); } [Fact] public void InlineChildElementsPrintedInline() { var doc = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var b = Element.Create("span"); doc.Body.AppendChild(b); doc.Body.AppendChild(new TextNode("hello")); var writer = new DocumentWriter(doc); writer.Print(); var result = writer.ToString(); Assert.Contains("/span>hello", result); } [Fact] public void AddDuplicateIdThrows() { var map = new DocumentIdMap(); var a1 = Element.Create("span", "a"); var a2 = Element.Create("span", "a"); map.NotifyAdded(a1); DomOperationsTesting.Throws(() => map.NotifyAdded(a2)); } [Fact] public void CheckedFalseFlushed() { var doc = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var x = new HtmlInputElement { Id = "x" }; doc.Body.AppendChild(x); x.Checked = true; doc.OpenEventQueue(); x.Checked = false; var queue = doc.GetQueue(); Assert.NotEmpty(queue); var top = queue.Peek() as SetCheckedDelta; Assert.NotNull(top); Assert.Equal(x.Id, top!.ElementId); Assert.False(top.Checked); } [Fact] public void CheckedTrueFlushed() { var doc = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var x = new HtmlInputElement { Id = "x" }; doc.Body.AppendChild(x); doc.OpenEventQueue(); x.Checked = true; var queue = doc.GetQueue(); Assert.NotEmpty(queue); var top = queue.Peek() as SetCheckedDelta; Assert.NotNull(top); Assert.Equal(x.Id, top!.ElementId); Assert.True(top.Checked); } [Fact] public void NotifyCheckedTrueAdds() { var div = Element.Create("div"); var a = new Attributes(div); a.NotifyChecked(true); Assert.True(a.HasAttribute("checked")); IEnumerator e = ((IEnumerable)a).GetEnumerator(); Assert.True(e.MoveNext()); a.NotifyChecked(false); Assert.False(a.HasAttribute("checked")); } } } ================================================ FILE: src/Tests/DOM/LaraBuilderTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.Collections.ObjectModel; using Xunit; namespace Integrative.Lara.Tests.DOM { [Obsolete("old methods")] public class LaraBuilderTesting : DummyContextTesting { private readonly Element _root; private readonly LaraBuilder _builder; public LaraBuilderTesting() { _root = Element.Create("div"); _builder = new LaraBuilder(_root); } [Fact] public async void OnStringAction() { var counter = 0; _builder.On("click", () => counter++); await _root.NotifyEvent("click"); Assert.Equal(1, counter); } [Fact] public void ToggleClassStringBool() { _builder.ToggleClass("red", true); Assert.Equal("red", _root.Class); } [Fact] public void ToggleClassString() { _builder.ToggleClass("blue"); Assert.Equal("blue", _root.Class); } [Fact] public void RemoveClass() { _root.Class = "very dark"; _builder.RemoveClass("very"); Assert.Equal("dark", _root.Class); } [Fact] public void AddClass() { _builder.AddClass("green"); Assert.Equal("green", _root.Class); } [Fact] public void GetCurrent() { _builder.GetCurrent(out var element); Assert.Same(_root, element); } [Fact] public void BindOptions() { var data = new MyData(); var found = false; _builder.Bind(new BindHandlerOptions { ModifiedHandler = (_, _) => found = true, BindObject = data }); data.Counter = 15; Assert.True(found); } [Fact] public void BindValueActions() { var data = new MyData(); var found = false; _builder.Bind(data, () => found = true); data.Counter = 15; Assert.True(found); } [Fact] public void BindChildrenOptions() { var list = new ObservableCollection(); _builder.BindChildren(new BindChildrenOptions(list, _ => Element.Create("span"))); list.Add(new MyData()); Assert.NotEmpty(_root.Children); } [Fact] public void BindChildrenCollectionElement() { var list = new ObservableCollection(); _builder.BindChildren(list, () => Element.Create("span")); list.Add(new MyData()); Assert.NotEmpty(_root.Children); } [Fact] public void BindChildenCollectionElement() { var list = new ObservableCollection(); _builder.BindChildren(list, _ => Element.Create("div")); list.Add(new MyData()); Assert.NotEmpty(_root.Children); } [Fact] public void BindInnerTextOptions() { var data = new MyData(); _builder.BindInnerText(new BindInnerTextOptions { BindObject = data, Property = x => x.Counter.ToString() }); data.Counter = 3; VerifyInnerText(_root, "3"); } private static void VerifyInnerText(Element element, string data) { Assert.NotEmpty(element.Children); var node = element.GetChildAt(0) as TextNode; Assert.NotNull(node); Assert.Equal(data, node!.Data); } [Fact] public void BindInnerTextExpanded() { var data = new MyData(); _builder.BindInnerText(data, () => data.Counter.ToString()); data.Counter = 3; VerifyInnerText(_root, "3"); } [Fact] public void BindInnerTextValueFuncString() { var data = new MyData(); _builder.BindInnerText(data, x => x.Counter.ToString()); data.Counter = 3; VerifyInnerText(_root, "3"); } [Fact] public void BindAttributeOptions() { var data = new MyData(); _builder.BindAttribute(new BindAttributeOptions { Attribute = "data-counter", BindObject = data, Property = x => x.Counter.ToString() }); data.Counter = 2; Assert.Equal("2", _root.GetAttribute("data-counter")); } [Fact] public void BindAttributeExpanded() { var data = new MyData(); _builder.BindAttribute("data-counter", data, () => data.Counter.ToString()); data.Counter = 2; Assert.Equal("2", _root.GetAttribute("data-counter")); } [Fact] public void BindAttributeStringFuncString() { var data = new MyData(); _builder.BindAttribute("data-counter", data, x => x.Counter.ToString()); data.Counter = 2; Assert.Equal("2", _root.GetAttribute("data-counter")); } [Fact] public void BindActionElement() { var data = new MyData(); _builder.Bind(data, (x, y) => y.InnerText = x.Counter.ToString()); data.Counter = 14; VerifyInnerText(_root, "14"); } private class MyData : BindableBase { private int _counter; public int Counter { get => _counter; set => SetProperty(ref _counter, value); } } [Fact] [Obsolete("old method")] public void AddTextNode1() { var div = Element.Create("div"); var builder = new LaraBuilder(div); builder.AddTextNode("@@", false); var node = div.GetChildAt(0) as TextNode; Assert.NotNull(node); Assert.Equal("@@", node!.Data); } [Fact] [Obsolete("old method")] public void AddTextNode2() { var div = Element.Create("div"); var builder = new LaraBuilder(div); var node = new TextNode("test"); builder.AddTextNode(node); var x = div.GetChildAt(0) as TextNode; Assert.Same(node, x); } [Fact] public void InnerText() { var div = Element.Create("div"); var builder = new LaraBuilder(div); builder.InnerText("a { BindObject = data, ClassName = "red", Property = x => x.Counter > 0 }); Assert.False(_root.HasClass("red")); data.Counter = 1; Assert.True(_root.HasClass("red")); data.Counter = 0; Assert.False(_root.HasClass("red")); } [Fact] public void ToggleClass2() { var data = new MyData(); _builder.BindToggleClass("red", data, () => data.Counter > 0); Assert.False(_root.HasClass("red")); data.Counter = 1; Assert.True(_root.HasClass("red")); data.Counter = 0; Assert.False(_root.HasClass("red")); } [Fact] public void ToggleClass3() { var data = new MyData(); _builder.BindToggleClass("red", data, x => x.Counter > 0); Assert.False(_root.HasClass("red")); data.Counter = 1; Assert.True(_root.HasClass("red")); data.Counter = 0; Assert.False(_root.HasClass("red")); } } } ================================================ FILE: src/Tests/Delta/AttributeEditTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using System; using Xunit; namespace Integrative.Lara.Tests.Delta { public class AttributeEditTesting : DummyContextTesting { [Fact] public void AttributeEdited() { var doc = CreateDocument(); var div = Element.Create("div", "mydiv"); doc.Body.AppendChild(div); doc.OpenEventQueue(); div.SetAttribute("data-test", "x"); var queue = doc.GetQueue(); Assert.Single(queue); var step = queue.Peek() as AttributeEditedDelta; Assert.Equal("data-test", step!.Attribute); Assert.Equal("x", step.Value); Assert.Equal("mydiv", step.ElementId); } [Fact] public void UnchangedIdNoSteps() { var doc = CreateDocument(); var div = Element.Create("div", "mydiv"); doc.Body.AppendChild(div); doc.OpenEventQueue(); div.Id = "mydiv"; var queue = doc.GetQueue(); Assert.Empty(queue); } [Fact] public void SetValueDeltaProperties() { var div = Element.Create("div", "mydiv"); var doc = CreateDocument(); doc.Body.AppendChild(div); doc.OpenEventQueue(); div.SetAttribute("value", "x"); var queue = doc.GetQueue(); Assert.NotEmpty(queue); var step = queue.Peek() as SetValueDelta; Assert.NotNull(step); Assert.Equal("mydiv", step!.ElementId); Assert.Equal("x", step.Value); } private static Document CreateDocument() { var page = new MyPage(); var doc = new Document(page, Connections.CreateCryptographicallySecureGuid(), BaseModeController.DefaultKeepAliveInterval); return doc; } [Fact] public void PlugOptionsHasEmptyConstructor() { var instance = Activator.CreateInstance(); Assert.NotNull(instance); } [Fact] public void ClearChildrenOnEvent() { var div = Element.Create("div"); var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); document.Body.AppendChild(div); document.OpenEventQueue(); document.Body.ClearChildren(); var queue = document.GetQueue(); Assert.NotEmpty(queue); var first = queue.Peek() as ClearChildrenDelta; Assert.NotNull(first); } [Fact] public void ToggleClassToggles() { var button = new HtmlButtonElement(); button.ToggleClass("red", true); Assert.True(button.HasClass("red")); button.ToggleClass("red", false); Assert.False(button.HasClass("red")); } [Fact] public void DocumentCreatesElements() { var button = Document.CreateElement("button"); Assert.NotNull(button); } [Fact] public void DocumentCreatesText() { var text = Document.CreateTextNode("hello"); Assert.NotNull(text); Assert.Equal("hello", text.Data); } [Fact] public void ClearChildrenElement() { var x = new ClearChildrenDelta { ElementId = "x" }; Assert.Equal("x", x.ElementId); } [Fact] public void PlugOptionsBlocking() { var settings = new EventSettings { BlockOptions = new BlockOptions { BlockedElementId = "a", ShowElementId = "b", ShowHtmlMessage = "c" } }; var x = new PlugOptions(settings); Assert.Equal("a", x.BlockElementId); Assert.Equal("b", x.BlockShownId); Assert.Equal("c", x.BlockHTML); } [Fact] public void ServerEventsDeltaCorrectType() { var delta = new ServerEventsDelta(); Assert.Equal(DeltaType.ServerEvents, delta.Type); } } } ================================================ FILE: src/Tests/Delta/DeltaTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 10/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.DOM; using Integrative.Lara.Tests.Main; using Integrative.Lara.Tests.Middleware; using Xunit; namespace Integrative.Lara.Tests.Delta { public class DeltaTesting : DummyContextTesting { [Fact] public void SubmitJsStores() { var x = new SubmitJsDelta { Code = "code" }; Assert.Equal("code", x.Code); } [Fact] public void ReplaceDeltaEnqueues() { var doc = DomOperationsTesting.CreateDocument(); doc.OpenEventQueue(); ReplaceDelta.Enqueue(doc, "test"); var q = doc.GetQueue(); Assert.Single(q); var first = q.Peek() as ReplaceDelta; Assert.NotNull(first); Assert.Equal("test", first!.Location); } [Fact] public void TextModifiedGenerated() { var doc = DomOperationsTesting.CreateDocument(); var span = Element.Create("span"); span.InnerText = "a"; doc.Body.AppendChild(span); doc.OpenEventQueue(); span.InnerText = "test"; var q = doc.GetQueue(); Assert.Single(q); var first = q.Peek() as TextModifiedDelta; Assert.NotNull(first); Assert.Equal("test", first!.Text); Assert.Equal(span.Id, first.ParentElementId); Assert.Equal(0, first.ChildNodeIndex); } [Fact] public void ElementEventValueData() { var x = new ElementEventValue { Checked = true, ElementId = "a", Value = "text" }; Assert.Equal("#a='text' checked", x.ToString()); x.Checked = false; Assert.Equal("#a='text'", x.ToString()); } [Fact] public void PlugOptionsLongRunning() { var x = new PlugOptions { LongRunning = true, Block = true }; Assert.True(x.LongRunning); Assert.True(x.Block); } [Fact] public void PlugOptionsSerialize() { var x = new PlugOptions { Block = true }; var json1 = x.ToJSON(); var json2 = LaraUI.JSON.Stringify(x); Assert.Equal(json1, json2); } [Fact] public void ClientEventFromSettings() { var x = new EventSettings { BlockOptions = new BlockOptions { BlockedElementId = "a", ShowElementId = "b", ShowHtmlMessage = "c" }, EventName = "click", LongRunning = true, Propagation = PropagationType.StopImmediatePropagation, DebounceInterval = 400, EvalFilter = "true", UploadFiles = true }; var client = ClientEventSettings.CreateFrom(x); client.ExtraData = "xx"; Assert.Equal(x.Block, client.Block); Assert.Equal(x.BlockOptions.ShowElementId, client.BlockShownId); Assert.Equal(x.BlockOptions.BlockedElementId, client.BlockElementId); Assert.Equal(x.BlockOptions.ShowHtmlMessage, client.BlockHTML); Assert.Equal(x.EventName, client.EventName); Assert.Equal(x.LongRunning, client.LongRunning); Assert.Equal(x.Propagation, client.Propagation); Assert.Equal(x.UploadFiles, client.UploadFiles); Assert.Equal("true", x.EvalFilter); Assert.Equal("xx", client.ExtraData); } [Fact] public void SubscribeDocumentEventEnqueues() { var doc = new Document(new MyPage(), 100); var settings = new EventSettings(); Assert.True(doc.CanDiscard); SubscribeDelta.Enqueue(doc, settings); Assert.False(doc.CanDiscard); var q = doc.GetQueue(); Assert.NotEmpty(q); } [Fact] public void DeltaPayload() { var x = new SubmitJsDelta { Payload = "abc" }; Assert.Equal("abc", x.Payload); } [Fact] public void DeltaRenderClass() { var document = new Document(new MyPage(), 100); RenderDelta.Enqueue(document, new []{document.Body}); var queue = document.GetQueue(); Assert.Single(queue); var delta = queue.Peek(); Assert.Equal(DeltaType.Render, delta.Type); var render = delta as RenderDelta; Assert.NotNull(render); Assert.NotNull(render?.Locator); Assert.NotNull(render?.Node); Assert.Equal(document.Body.Id, render?.Locator?.StartingId); } } } ================================================ FILE: src/Tests/Delta/LocatorTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using Xunit; namespace Integrative.Lara.Tests.Delta { public class LocatorTesting : DummyContextTesting { [Fact] public void LocateElementWithId() { var x = Element.Create("span", "x"); var locator = NodeLocator.FromNode(x); Assert.Equal(x.Id, locator.StartingId); } } } ================================================ FILE: src/Tests/Main/ButtonCounterPage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System.Threading.Tasks; namespace Integrative.Lara.Tests.Main { internal class ButtonCounterPage : IPage { private const string ButtonId = "MyCounterButton"; private readonly bool _useSockets; public ButtonCounterPage(bool useSockets) { _useSockets = useSockets; } private int _counter; public Task OnGet() { var document = LaraUI.Page.Document; var span = Element.Create("span"); var text = new TextNode("Click me"); var button = Element.Create("button", ButtonId); button.AppendChild(text); document.Body.AppendChild(span); document.Body.AppendChild(button); button.On(new EventSettings { EventName = "click", LongRunning = _useSockets, Handler = () => { _counter++; text.Data = $"Clicked {_counter} times"; return Task.CompletedTask; } }); return Task.CompletedTask; } } } ================================================ FILE: src/Tests/Main/ConnectionTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System.Net; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Main { public class ConnectionTesting : DummyContextTesting { [Fact] public void NonExistingDocument() { var guid = Connections.CreateCryptographicallySecureGuid(); var connection = new Connection(guid, IPAddress.Loopback); Assert.True(IPAddress.Loopback.Equals(connection.RemoteIp)); Assert.True(connection.IsEmpty); Assert.False(connection.TryGetDocument(guid, out _)); } [Fact] public void CreateDocumentPresent() { var connectionId = Connections.CreateCryptographicallySecureGuid(); var connection = new Connection(connectionId, IPAddress.Loopback); var document = connection.CreateDocument(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var count = 0; foreach (var pair in connection.GetDocuments()) { Assert.Same(document, pair.Value); count++; } Assert.Equal(1, count); Assert.True(connection.TryGetDocument(document.VirtualId, out var found)); Assert.Same(document, found); Assert.False(connection.IsEmpty); } [Fact] public async void DiscardRemovesDocument() { var connectionId = Connections.CreateCryptographicallySecureGuid(); var connection = new Connection(connectionId, IPAddress.Loopback); var page = new MyPage(); var document = connection.CreateDocument(page, BaseModeController.DefaultKeepAliveInterval); await connection.Discard(document.VirtualId); Assert.False(connection.TryGetDocument(document.VirtualId, out _)); Assert.True(page.Disposed); } [Fact] public void CanDiscardStartsFalse() { var x = CreateDocument(); Assert.True(x.CanDiscard); } [Fact] public void CannotDiscardAfterServerEventsOn() { var x = CreateDocument(); x.ServerEventsOn(); Assert.False(x.CanDiscard); } [Fact] public void CannotDiscardAfterEvent() { var x = CreateDocument(); var div = Element.Create("div"); div.On("click", () => Task.CompletedTask); x.Body.AppendChild(div); Assert.False(x.CanDiscard); } private static Document CreateDocument() { var connectionId = Connections.CreateCryptographicallySecureGuid(); var connection = new Connection(connectionId, IPAddress.Loopback); return connection.CreateDocument(new MyPage(), 100); } } } ================================================ FILE: src/Tests/Main/ConnectionsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.Net; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Main { public class ConnectionsTesting : DummyContextTesting { [Fact] public void ConnectionFound() { using var connections = new Connections(); var cnx = connections.CreateConnection(IPAddress.Loopback); var count = 0; foreach (var unused in connections.GetConnections()) { count++; } Assert.Equal(1, count); Assert.True(connections.TryGetConnection(cnx.Id, out var found)); Assert.Same(cnx, found); } [Fact] public async void DiscardRemovesConnection() { using var connections = new Connections(); var cnx = connections.CreateConnection(IPAddress.Loopback); await connections.Discard(cnx.Id); Assert.False(connections.TryGetConnection(cnx.Id, out _)); } [Fact] public void DisposeRemovesConnection() { var connections = new Connections(); var cnx = connections.CreateConnection(IPAddress.Loopback); connections.Dispose(); Assert.False(connections.TryGetConnection(cnx.Id, out _)); } [Fact] public async void TimerCleansUpDocuments() { using var connections = new Connections(); var connection = connections.CreateConnection(IPAddress.Loopback); connection.CreateDocument(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var required = DateTime.UtcNow.AddSeconds(10); Assert.NotEmpty(connection.GetDocuments()); await StaleConnectionsCollector.CleanupExpired(connection, required); Assert.Empty(connection.GetDocuments()); } [Fact] public async void TimerCleansUp() { using var connections = new Connections(200, 100); var cnx = connections.CreateConnection(IPAddress.Loopback); var document = cnx.CreateDocument(new MyPage(), BaseModeController.DefaultKeepAliveInterval); Assert.NotEmpty(connections.GetConnections()); document.ModifyLastUtcForTesting(DateTime.UtcNow.AddDays(-1)); await Task.Delay(400); Assert.Empty(connections.GetConnections()); } } } ================================================ FILE: src/Tests/Main/MyPage.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Threading.Tasks; namespace Integrative.Lara.Tests.Main { internal class MyPage : IPage, IDisposable { public bool Disposed { get; private set; } public void Dispose() { Disposed = true; } public Task OnGet() { throw new NotImplementedException(); } } } ================================================ FILE: src/Tests/Main/PublishedTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using System; using System.Net; using System.Runtime.Serialization; using System.Threading.Tasks; using Integrative.Lara.Tests.Middleware; using Microsoft.AspNetCore.Http; using Moq; using Xunit; namespace Integrative.Lara.Tests.Main { public class PublishedTesting : DummyContextTesting { [Fact] public void UnpublishRemoves() { using var app = new Application(); using var published = app.GetPublished(); published.Publish("/coco", new StaticContent(new byte[0])); published.Publish("/lala", new StaticContent(new byte[0])); app.UnPublish("/coco"); Assert.True(published.TryGetNode("/lala", out _)); Assert.False(published.TryGetNode("/coco", out _)); } [Fact] public async void RedirectExecutes() { var http = new Mock(); var response = new Mock(); var request = new Mock(); http.Setup(x => x.Response).Returns(response.Object); http.Setup(x => x.Request).Returns(request.Object); request.Setup(x => x.Method).Returns("GET"); var page = new MyRedirectPage(); var document = new Document(page, BaseModeController.DefaultKeepAliveInterval); var cx = new Connection(Guid.NewGuid(), IPAddress.Loopback); var context = new PageContext(Context.Application, http.Object, cx); await page.OnGet(); await PagePublished.ProcessGetResult(http.Object, document, context, HttpStatusCode.OK); response.Verify(x => x.Redirect("https://www.google.com")); } private class MyRedirectPage : IPage { public Task OnGet() { LaraUI.Page.Navigation.Replace("https://www.google.com"); return Task.CompletedTask; } } [Fact] public void WebServiceContentType() { var x = new WebServiceContent(); Assert.Equal("application/json", x.ContentType); Assert.Equal("POST", x.Method); x.ContentType = "text/html"; x.Method = "PUT"; Assert.Equal("text/html", x.ContentType); Assert.Equal("PUT", x.Method); } [Fact] public void LaraStringify() { var start = new MyClass { Value = 5 }; var json = LaraUI.JSON.Stringify(start); var ok = LaraUI.JSON.TryParse(json, out var result); Assert.True(ok); Assert.Equal(start.Value, result!.Value); } [DataContract] private class MyClass { [DataMember] public int Value { get; set; } } [Fact] public void SessionRemoveValue() { var guid = Guid.Parse("{A11072B8-7CD4-4D70-821D-C8934ACCD270}"); var connection = new Connection(guid, IPAddress.Loopback); var session = new Session(connection); session.SaveValue("mykey", "myvalue"); session.RemoveValue("mykey"); Assert.False(session.TryGetValue("mykey", out _)); } } } ================================================ FILE: src/Tests/Main/StaleTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.Net; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Main { public class StaleTesting : DummyContextTesting { [Fact] public async void CleanupLeavesUnexpiredDocument() { var connections = new Connections(); var cnx = connections.CreateConnection(IPAddress.Loopback); var doc1 = cnx.CreateDocument(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var doc2 = cnx.CreateDocument(new MyPage(), BaseModeController.DefaultKeepAliveInterval); doc2.ModifyLastUtcForTesting(DateTime.UtcNow.AddHours(-10)); await Task.Delay(200); using (var collector = new StaleConnectionsCollector(connections)) { await collector.CleanupExpiredHandler(); } Assert.True(cnx.TryGetDocument(doc1.VirtualId, out _)); Assert.False(cnx.TryGetDocument(doc2.VirtualId, out _)); } [Fact] public async void EmptyConnectionGetsCollected() { var connections = new Connections(); var cnx = connections.CreateConnection(IPAddress.Loopback); using (var collector = new StaleConnectionsCollector(connections)) { await collector.CleanupExpiredHandler(); } Assert.False(connections.TryGetConnection(cnx.Id, out _)); } [Fact] public void TimerInterval() { using var x = new Connections { StaleCollectionInterval = 100, StaleExpirationInterval = 200 }; Assert.Equal(100, x.StaleCollectionInterval); Assert.Equal(200, x.StaleExpirationInterval); } } } ================================================ FILE: src/Tests/Main/StaticContentTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Middleware; using System; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using Xunit; namespace Integrative.Lara.Tests.Main { public class StaticContentTesting : DummyContextTesting { private static readonly HttpClient _Client; static StaticContentTesting() { var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate }; _Client = new HttpClient(handler); } [Fact] public void AreadyCompressedFileDoesNotGetCompressed() { var bytes = LoadSampleJpeg(); var content = new StaticContent(bytes, ContentTypes.ImageJpeg); Assert.Same(bytes, content.GetBytes()); Assert.Equal(ContentTypes.ImageJpeg, content.ContentType); Assert.False(content.Compressed); Assert.False(string.IsNullOrEmpty(content.ETag)); } private static byte[] LoadSampleJpeg() { return LoadAsset("pexels-photo-248673.jpeg"); } private static byte[] LoadCompressibleBmp() { return LoadAsset("Compressible.bmp"); } private static byte[] LoadAsset(string filename) { return LoadResource($"Integrative.Lara.Tests.Assets.{filename}"); } private static byte[] LoadResource(string filename) { var assembly = Assembly.GetAssembly(typeof(StaticContentTesting)); using Stream? resFilestream = assembly!.GetManifestResourceStream(filename); if (resFilestream == null) return Array.Empty(); byte[] ba = new byte[resFilestream.Length]; resFilestream.Read(ba, 0, ba.Length); return ba; } [Fact] public async void RequestWithoutETagReceivesFile() { var bytes = LoadSampleJpeg(); var content = new StaticContent(bytes, ContentTypes.ImageJpeg); await Context.Application.Start(); var address = LaraUI.GetFirstURL(Context.Application.GetHost()); Context.Application.PublishFile("/", content); using var response = await _Client.GetAsync(address); var downloaded = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Headers.TryGetValues("ETag", out var values)); Assert.Equal(content.ETag, values?.FirstOrDefault()); Assert.Equal(bytes, downloaded); } [Fact] public async void RequestWrongETagReceivesFile() { var bytes = LoadSampleJpeg(); var content = new StaticContent(bytes, ContentTypes.ImageJpeg); await Context.Application.Start(new StartServerOptions { AllowLocalhostOnly = true }); var address = LaraUI.GetFirstURL(Context.Application.GetHost()); Context.Application.PublishFile("/", content); var request = new HttpRequestMessage { Method = new HttpMethod("GET"), RequestUri = new Uri(address) }; request.Headers.TryAddWithoutValidation("If-None-Match", "lalalalala"); using var response = await _Client.SendAsync(request); var downloaded = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Headers.TryGetValues("ETag", out var values)); Assert.Equal(content.ETag, values?.FirstOrDefault()); Assert.Equal(bytes, downloaded); } [Fact] public async void RequestCorrectETagReceivesNotModified() { var bytes = LoadSampleJpeg(); var content = new StaticContent(bytes, ContentTypes.ImageJpeg); await Context.Application.Start(); var address = LaraUI.GetFirstURL(Context.Application.GetHost()); Context.Application.PublishFile("/", content); var request = new HttpRequestMessage { Method = new HttpMethod("GET"), RequestUri = new Uri(address) }; request.Headers.TryAddWithoutValidation("If-None-Match", content.ETag); using var response = await _Client.SendAsync(request); var downloaded = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); Assert.False(response.Headers.Contains("ETag")); Assert.Empty(downloaded); } [Fact] public async void CompressibleFileIsSentCompressed() { var bytes = LoadCompressibleBmp(); var content = new StaticContent(bytes, "image"); await Context.Application.Start(); var address = LaraUI.GetFirstURL(Context.Application.GetHost()); Context.Application.PublishFile("/", content); using var response = await _Client.GetAsync(address); var downloaded = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.True(response.Headers.TryGetValues("ETag", out var values)); Assert.Equal(content.ETag, values?.FirstOrDefault()); Assert.Equal(bytes, downloaded); } [Fact] public async void ContentNotFound() { await Context.Application.Start(); var address = LaraUI.GetFirstURL(Context.Application.GetHost()); using var response = await _Client.GetAsync(address + "/lalala"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } } ================================================ FILE: src/Tests/Middleware/DummyContext.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using Moq; using System; using System.Diagnostics.CodeAnalysis; using System.Net; namespace Integrative.Lara.Tests.Middleware { internal class DummyContext : BaseContext, IPageContext, IWebServiceContext { private DummyContext(Application app, Mock http) : base(app, http.Object) { var request = new Mock(); http.Setup(x => x.Request).Returns(request.Object); request.Setup(x => x.Path).Returns("/abc"); var guid = Connections.CreateCryptographicallySecureGuid(); var cnx = new Connection(guid, IPAddress.Loopback); Session = new Session(cnx); JSBridge = _bridge.Object; } public Document Document => throw new NotImplementedException(); private readonly Mock _bridge = new Mock(); public IJsBridge JSBridge { get; set; } public INavigation Navigation => throw new NotImplementedException(); public Session Session { get; } public string RequestBody { get; set; } = string.Empty; public HttpStatusCode StatusCode { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public static DummyContext Create() { var app = new Application(); var http = new Mock(); return new DummyContext(app, http); } public void Dispose() { Application.Dispose(); } public bool TryGetSession([NotNullWhen(true)] out Session? session) { throw new NotImplementedException(); } } } ================================================ FILE: src/Tests/Middleware/DummyContextTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 11/2019 Author: Pablo Carbonell */ using System; namespace Integrative.Lara.Tests.Middleware { public class DummyContextTesting : IDisposable { internal readonly DummyContext Context = DummyContext.Create(); public void Dispose() { Context.Dispose(); } } } ================================================ FILE: src/Tests/Middleware/ErrorPagesTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using Xunit; namespace Integrative.Lara.Tests.Middleware { public class ErrorPagesTesting : DummyContextTesting { [Fact] public void DefaultNotFoundRuns() { var pages = new ErrorPages(Context.Application.GetPublished()); var page = pages.GetPage(System.Net.HttpStatusCode.NotFound); Assert.NotNull(page); } [Fact] public void DefaultServerErrorRuns() { var pages = new ErrorPages(Context.Application.GetPublished()); var found = pages.TryGetPage(System.Net.HttpStatusCode.InternalServerError, out var page); Assert.True(found); Assert.NotNull(page); } } } ================================================ FILE: src/Tests/Middleware/EventParametersTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 12/2019 Author: Pablo Carbonell */ using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using Xunit; namespace Integrative.Lara.Tests.Middleware { public class EventParametersTesting { private readonly Guid _id = Guid.Parse("{2F97BE9D-1CC3-4EBD-BC93-EF748B774F1D}"); [Fact] public void RoundTripMinimum() { var x = new EventParameters { DocumentId = _id, ElementId = "abc", EventName = "click", EventNumber = 1, }; var json = LaraUI.JSON.Stringify(x); var result = LaraUI.JSON.Parse(json); Assert.Equal(x.DocumentId, result.DocumentId); Assert.Equal(x.ElementId, result.ElementId); Assert.Equal(x.EventName, result.EventName); Assert.Equal(x.EventNumber, result.EventNumber); } [Fact] public void RoundTripEmptyFiles() { var x = new SocketEventParameters { DocumentId = _id, SocketFiles = new FormFileCollection() }; var json = LaraUI.JSON.Stringify(x); var result = LaraUI.JSON.Parse(json); Assert.NotNull(result.SocketFiles); Assert.Empty(result.SocketFiles); } [Fact] public void RoundTripFile() { var x = new FormFile { Content = "hello", ContentDisposition = "a", ContentType = "b", FileName = "c", Name = "d" }; var json = LaraUI.JSON.Stringify(x); var result = LaraUI.JSON.Parse(json); Assert.Equal(x.Content, result.Content); Assert.Equal(x.ContentDisposition, result.ContentDisposition); Assert.Equal(x.ContentType, result.ContentType); Assert.Equal(x.FileName, result.FileName); Assert.Equal(x.Length, result.Length); Assert.Equal(x.Name, result.Name); } [Fact] public void SerializeBytes() { var bytes = BuildBytes(); var x = new FormFile { Content = Convert.ToBase64String(bytes) }; var json = LaraUI.JSON.Stringify(x); var result = LaraUI.JSON.Parse(json); var output = Convert.FromBase64String(result.Content); Assert.Equal(256, bytes.Length); for (var index = 0; index < 256; index++) { Assert.Equal(index, output[index]); } } private static byte[] BuildBytes() { var bytes = new byte[256]; for (var index = 0; index <= 255; index++) { bytes[index] = (byte)index; } return bytes; } [Fact] public void FileCollectionIterates() { var f1 = new FormFile(); var f2 = new FormFile(); var list = new FormFileCollection { InnerList = new List() }; list.InnerList.Add(f1); list.InnerList.Add(f2); var other = new List(); foreach (FormFile? f in list) { Assert.NotNull(f); other.Add(f!); } Assert.Equal(2, other.Count); Assert.Same(f1, other[0]); Assert.Same(f2, other[1]); } [Fact] public void NullCollectionEmpty() { var x = new FormFileCollection(); var size = x.Count; Assert.Equal(0, size); x.InnerList = new List(); size = x.Count; Assert.Equal(0, size); } [Fact] public void FindFileByName() { var f1 = new FormFile(); var f2 = new FormFile { Name = "lala" }; var x = new FormFileCollection { InnerList = new List { f1, f2 } }; var found = x.GetFile("lala"); Assert.Same(f2, found); } [Fact] public void GetFileReadonlyList() { var f1 = new FormFile(); var x = new FormFileCollection { InnerList = new List { f1 } }; var list = x as IReadOnlyList; var found = list[0]; Assert.Same(f1, found); } [Fact] public void GetFilesByName() { var f1 = new FormFile { Name = "a" }; var f2 = new FormFile { Name = "b" }; var f3 = new FormFile { Name = "a" }; var x = new FormFileCollection { InnerList = new List { f1, f2, f3 } }; var found = x.GetFiles("a"); Assert.Equal(2, found.Count); Assert.Same(f1, found[0]); Assert.Same(f3, found[1]); } [Fact] public void EnumrableInterface() { var x = new FormFileCollection(); var y = (IEnumerable) x; using var enumerator = y.GetEnumerator(); Assert.False(enumerator.MoveNext()); } } } ================================================ FILE: src/Tests/Middleware/MiddlewareTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 5/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.DOM; using Integrative.Lara.Tests.Main; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Moq; using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Middleware { internal class MyStatusPage : IPage { public Task OnGet() { throw new StatusCodeException(HttpStatusCode.Unauthorized); } } public class MiddlewareTesting : DummyContextTesting { [Fact] public void TryParseMissingDocFails() { StringValues values; var mock = new Mock(); mock.Setup(x => x.TryGetValue("doc", out values)).Returns(false); Assert.False(DiscardParameters.TryParse(mock.Object, out _)); Assert.False(EventParameters.TryParse(mock.Object, out _)); } [Fact] public async void LocalhostFilterTesting() { var logger = new Mock>(); var next = new Mock(); var filter = new LocalhostFilter(next.Object, logger.Object); var connectionInfo = new Mock(); connectionInfo.Setup(x => x.RemoteIpAddress).Returns(IPAddress.Parse("172.217.4.206")); var http = new Mock(); http.Setup(x => x.Connection).Returns(connectionInfo.Object); var response = new Mock(); http.Setup(x => x.Response).Returns(response.Object); response.SetupProperty(x => x.StatusCode); var headers = new Mock(); response.Setup(x => x.Headers).Returns(headers.Object); var body = new Mock(); response.Setup(x => x.Body).Returns(body.Object); response.SetupProperty(x => x.ContentLength); await filter.Invoke(http.Object); Assert.Equal(403, response.Object.StatusCode); } [Fact] public async void NullBodyReturnsEmptyString() { var http = new Mock(); var request = new Mock(); http.Setup(x => x.Request).Returns(request.Object); var result = await MiddlewareCommon.ReadBody(http.Object); Assert.Equal(string.Empty, result); } [Fact] public void AddHeaderNeverExpires() { var http = new Mock(); var response = new Mock(); http.Setup(x => x.Response).Returns(response.Object); var headers = new Mock(); response.Setup(x => x.Headers).Returns(headers.Object); MiddlewareCommon.AddHeaderNeverExpires(http.Object); headers.Verify(x => x.Add("Cache-Control", "max-age=31556926"), Times.Exactly(1)); } [Fact] public void ProcessMessageSkipsEmptyMessage() { var http = new Mock(); var cx = new Connection(Guid.NewGuid(), IPAddress.Loopback); var context = new PageContext(Context.Application, http.Object, cx); var parameters = new EventParameters(); PostEventHandler.ProcessMessageIfNeeded(context, parameters); } [Fact] public async void ConnectionNotFoundSendsReload() { var http = new Mock(); var request = new Mock(); var cookies = new Mock(); var response = new Mock(); var headers = new Mock(); var body = new Mock(); var sockets = new Mock(); http.Setup(x => x.Request).Returns(request.Object); http.Setup(x => x.Response).Returns(response.Object); http.Setup(x => x.WebSockets).Returns(sockets.Object); request.Setup(x => x.Method).Returns("POST"); request.Setup(x => x.Path).Returns("/_event"); request.Setup(x => x.Cookies).Returns(cookies.Object); response.Setup(x => x.Headers).Returns(headers.Object); response.Setup(x => x.Body).Returns(body.Object); sockets.Setup(x => x.IsWebSocketRequest).Returns(false); var query = new MyQueryCollection(); request.Setup(x => x.Query).Returns(query); query.Add("doc", "EF2FF98720E34A2EA29E619977A5F04A"); query.Add("el", "lala"); query.Add("ev", "lala"); var next = new Mock(); var handler = new PostEventHandler(Context.Application, next.Object); var result = await handler.ProcessRequest(http.Object); Assert.True(result); } private class MyQueryCollection : IQueryCollection { private readonly Dictionary _map = new Dictionary(); public StringValues this[string key] => _map[key]; public int Count => _map.Count; public ICollection Keys => _map.Keys; public bool ContainsKey(string key) => _map.ContainsKey(key); public IEnumerator> GetEnumerator() { return _map.GetEnumerator(); } public bool TryGetValue(string key, out StringValues value) { return _map.TryGetValue(key, out value); } IEnumerator IEnumerable.GetEnumerator() { return _map.GetEnumerator(); } public void Add(string key, string value) { _map.Add(key, new StringValues(value)); } } [Fact] public void ClientEventMessageExtraData() { var x = new ClientEventMessage { ExtraData = "lala" }; Assert.Equal("lala", x.ExtraData); } [Fact] [Obsolete] public void UseLaraEmpty() { var app = new Mock(); Assert.Same(app.Object, app.Object.UseLara()); } [Fact] public void PageContextSocket() { var http = new Mock(); var cx = new Connection(Guid.NewGuid(), IPAddress.Loopback); var page = new PageContext(Context.Application, http.Object, cx); var socket = new Mock(); page.Socket = socket.Object; Assert.Same(socket.Object, page.Socket); } [Fact] public async void CannotFlushAjax() { var http = new Mock(); var cx = new Connection(Guid.NewGuid(), IPAddress.Loopback); var page = new PageContext(Context.Application, http.Object, cx); await DomOperationsTesting.ThrowsAsync(async () => await page.Navigation.FlushPartialChanges()); } [Fact] public async void FlushSendsMessage() { var http = new Mock(); var document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); var socket = new Mock(); var cx = new Connection(Guid.NewGuid(), IPAddress.Loopback); var page = new PageContext(Context.Application, http.Object, cx) { Socket = socket.Object, DocumentInternal = document }; var button = new HtmlButtonElement(); document.OpenEventQueue(); document.Body.AppendChild(button); await page.Navigation.FlushPartialChanges(); Assert.Empty(document.GetQueue()); Assert.Same(socket.Object, page.Socket); } [Fact] public void EmptyArraySegment() { var x = PostEventHandler.BuildArraySegment(""); Assert.NotNull(x.Array); Assert.Empty(x.Array); } [Fact] public void ValidateAddress() { Published.ValidateAddress("test"); Assert.ThrowsAny(() => Published.ValidateAddress("")); Assert.ThrowsAny(() => Published.ValidateAddress(null)); } [Fact] public void ValidateMethod() { Published.ValidateMethod("test"); Assert.ThrowsAny(() => Published.ValidateMethod("")); Assert.ThrowsAny(() => Published.ValidateMethod(null)); } [Fact] public void UnpublishMethod() { var service = new Mock(); using var x = new Published(); x.Publish(new WebServiceContent { Address = "/myws", Method = "PUT", Factory = () => service.Object }); var combined = Published.CombinePathMethod("/myws", "PUT"); Assert.True(x.TryGetNode(combined, out _)); x.UnPublish("/myws", "PUT"); Assert.False(x.TryGetNode(combined, out _)); } [Fact] public void WebServiceSessionNotFound() { var http = new Mock(); var context = new WebServiceContext(Context.Application, http.Object); var request = new Mock(); http.Setup(x => x.Request).Returns(request.Object); var cookies = new Mock(); request.Setup(x => x.Cookies).Returns(cookies.Object); string temp; cookies.Setup(x => x.TryGetValue(GlobalConstants.CookieSessionId, out temp)).Returns(false); Assert.False(context.TryGetSession(out _)); } [Fact] public void WebServiceCustomCode() { var http = new Mock(); var x = new WebServiceContext(Context.Application, http.Object) { StatusCode = HttpStatusCode.BadRequest }; Assert.Equal(HttpStatusCode.BadRequest, x.StatusCode); } [Fact] public async void PostEventHandlerSkipsRequests() { var http = new Mock(); var next = new Mock(); var post = new PostEventHandler(Context.Application, next.Object); var request = new Mock(); http.Setup(x => x.Request).Returns(request.Object); request.Setup(x => x.Path).Returns(PostEventHandler.EventPrefix); request.Setup(x => x.Method).Returns("LALA"); var websockets = new Mock(); http.Setup(x => x.WebSockets).Returns(websockets.Object); Assert.False(await post.ProcessRequest(http.Object)); } [Fact] public async void PostEventHandlerNoElement() { var http = new Mock(); var page = new MyPage(); var document = new Document(page, BaseModeController.DefaultKeepAliveInterval); var context = new PostEventContext(Context.Application, http.Object) { Document = document, Parameters = new EventParameters { ElementId = "aaa" } }; var sockets = new Mock(); http.Setup(x => x.WebSockets).Returns(sockets.Object); var response = new Mock(); http.Setup(x => x.Response).Returns(response.Object); response.SetupProperty(x => x.StatusCode); var headers = new Mock(); response.Setup(x => x.Headers).Returns(headers.Object); var body = new Mock(); response.Setup(x => x.Body).Returns(body.Object); await PostEventHandler.ProcessRequestDocument(context); var code = response.Object.StatusCode; Assert.True(code == 200 || code == 0); } [Fact] public async void SendReplyLeavesSocketOpen() { var page = new MyPage(); var document = new Document(page, BaseModeController.DefaultKeepAliveInterval); document.ServerEventsOn(); var post = new Mock(Context.Application, Context.Http); post.Object.Document = document; var parameters = new EventParameters { EventName = GlobalConstants.ServerSideEvent, }; post.Object.Parameters = parameters; var http = new Mock(); var ws = new Mock(); http.Setup(x => x.WebSockets).Returns(ws.Object); ws.Setup(x => x.IsWebSocketRequest).Returns(true); post.Object.Http = http.Object; post.Setup(x => x.GetSocketCompletion()).Returns(CompletionResult); await PostEventHandler.SendReply(post.Object, "lala"); post.Verify(x => x.GetSocketCompletion()); } private static Task> CompletionResult() { var mock = new Mock>(); return Task.FromResult(mock.Object); } [Fact] public async void StatusCodeExceptionProcessed() { var element = Element.Create("div"); element.On("click", () => throw new StatusCodeException(HttpStatusCode.Forbidden)); var http = new Mock(); var post = new PostEventContext(Context.Application, http.Object) { Element = element, Parameters = new EventParameters { EventName = "click" } }; var response = new Mock(); response.SetupProperty(x => x.StatusCode); http.Setup(x => x.Response).Returns(response.Object); post.Http = http.Object; var headers = new Mock(); response.Setup(x => x.Headers).Returns(headers.Object); var body = new Mock(); response.Setup(x => x.Body).Returns(body.Object); await PostEventHandler.RunEventHandler(post); Assert.Equal((int)HttpStatusCode.Forbidden, response.Object.StatusCode); } [Fact] public void ProcessSocketMessageWrongType() { var ms = new Mock(); var sr = new WebSocketReceiveResult(1, WebSocketMessageType.Close, true); var result = MiddlewareCommon.ProcessWebSocketMessage(100, ms.Object, sr); Assert.False(result.Item1); } [Fact] public void ProcessSocketMessageWrongCount() { var ms = new Mock(); var sr = new WebSocketReceiveResult(101, WebSocketMessageType.Text, true); var result = MiddlewareCommon.ProcessWebSocketMessage(100, ms.Object, sr); Assert.False(result.Item1); } [Fact] public void ProcessSocketMessageFailDeserialize() { var bytes = Encoding.UTF8.GetBytes("hello"); using var ms = new MemoryStream(bytes); var sr = new WebSocketReceiveResult(1, WebSocketMessageType.Text, true); var result = MiddlewareCommon.ProcessWebSocketMessage(100, ms, sr); Assert.False(result.Item1); } [Fact] public async void ProcessAjaxBadParameters() { var http = new Mock(); var request = new Mock(); var response = new Mock(); var query = new Mock(); var headers = new Mock(); http.Setup(x => x.Request).Returns(request.Object); request.Setup(x => x.Query).Returns(query.Object); StringValues values; query.Setup(x => x.TryGetValue("doc", out values)).Returns(false); response.SetupProperty(x => x.StatusCode); http.Setup(x => x.Response).Returns(response.Object); response.Setup(x => x.Headers).Returns(headers.Object); var body = new Mock(); response.Setup(x => x.Body).Returns(body.Object); await PostEventHandler.ProcessAjaxRequest(Context.Application, http.Object); } [Fact] public void PostGetCompletionRuns() { var page = new MyPage(); var document = new Mock(page, 200); var socket = new Mock(); var post = new PostEventContext(Context.Application, Context.Http) { Document = document.Object, Socket = socket.Object }; var task = post.GetSocketCompletion(); Assert.NotNull(task); document.Verify(x => x.GetSocketCompletion(socket.Object)); } [Fact] public void ServerEventCases() { var socket = new Mock(); var status = ServerEventsController.CalculateServerEventsStatus(false, socket.Object); Assert.Equal(ServerEventsStatus.Disabled, status); status = ServerEventsController.CalculateServerEventsStatus(true, null); Assert.Equal(ServerEventsStatus.Connecting, status); status = ServerEventsController.CalculateServerEventsStatus(true, socket.Object); Assert.Equal(ServerEventsStatus.Enabled, status); } [Fact] public void SessionCloseIgnoresErrors() { using var connections = new Connections(); var connection = connections.CreateConnection(IPAddress.Loopback); var session = new Session(connection); session.Closing += Session_Closing; session.Close(); } private static void Session_Closing(object? sender, EventArgs e) { throw new InvalidOperationException(); } [Fact] public void LaraPageDefaultConstructor() { var page = new LaraPageAttribute(); Assert.True(string.IsNullOrEmpty(page.Address)); } [Fact] public void ServerLauncherUseDeveloperPage() { var lara = new Application(); var app = new Mock(); ServerLauncher.ConfigureExceptions(app.Object, lara, new StartServerOptions { ShowExceptions = true }); app.Verify(x => x.ApplicationServices, Times.Once); } [Fact] public void LaraCreateConnection() { using var app = new Application(); app.CreateModeController(ApplicationMode.Default); var x = app.CreateConnection(IPAddress.Loopback); var ok = app.TryGetConnection(x.Id, out var y); Assert.True(ok); Assert.Same(x, y); app.ClearEmptyConnection(x); Assert.False(app.TryGetConnection(x.Id, out _)); } [Fact] public void SetDefaultErrorPage() { var published = Context.Application.GetPublished(); var x = new ErrorPages(published); var page = new MyPage(); x.SetDefaultPage(HttpStatusCode.ExpectationFailed, () => page); var result = x.GetPage(HttpStatusCode.ExpectationFailed); var show = result.CreateInstance(); Assert.Same(page, show); x.Remove(HttpStatusCode.ExpectationFailed); result = x.GetPage(HttpStatusCode.ExpectationFailed); Assert.Null(result); } [Fact] public void DefaultErrorPageReturned() { var published = Context.Application.GetPublished(); var x = new ErrorPages(published); var page = x.DefaultServerError(); Assert.True(page is DefaultErrorPage); } [Fact] public void DefaultNotFoundReturned() { var x = new ErrorPages(Context.Application.GetPublished()); var page = x.DefaultNotFound(); Assert.True(page is DefaultErrorPage); } [Fact] public void SequencerReorders() { var builder = new StringBuilder(); var x = new Sequencer(); Task.Run(async () => { var ok = await x.WaitForTurn(2); Assert.True(ok); AddChars(builder, "a"); }); Task.Run(async () => { var ok = await x.WaitForTurn(1); Assert.True(ok); AddChars(builder, "b"); }); Task.Run(async () => { var ok = await x.WaitForTurn(3); Assert.True(ok); Assert.Equal("ba", builder.ToString()); }); } [Fact] public void SequencerAborts() { var x = new Sequencer(); Task.Run(async () => { var ok = await x.WaitForTurn(3); Assert.False(ok); }); Task.Run(async () => { var ok = await x.WaitForTurn(1); Assert.True(ok); x.AbortAll(); }); } private static void AddChars(StringBuilder builder, string text) { builder.Append(text); } [Fact] public void ApplicationModeControllerProperties() { using var app = new Application(); app.CreateModeController(ApplicationMode.BrowserApp); Assert.Equal(BrowserAppController.BrowserAppKeepAliveInterval, app.KeepAliveInterval); Assert.True(app.AllowLocalhostOnly); } [Fact] public async void LocalhostFilterPass() { var http = new Mock(); var cnx = new Mock(); http.Setup(x => x.Connection).Returns(cnx.Object); cnx.Setup(x => x.RemoteIpAddress).Returns(IPAddress.Loopback); var next = new Mock(); var logger = new Mock>(); var filter = new LocalhostFilter(next.Object, logger.Object); await filter.Invoke(http.Object); next.Verify(x => x.Invoke(http.Object), Times.Once); } [Fact] public void StartServerOptionsSettings() { var x = new StartServerOptions { Port = 12345, IPAddress = IPAddress.Loopback }; Assert.Equal(12345, x.Port); Assert.Equal(IPAddress.Loopback, x.IPAddress); } private static readonly object _MyLock = new object(); [Fact] [Obsolete] public void LaraUiDefaultStatic() { lock (_MyLock) { LaraUI.Publish("/a", new StaticContent(Encoding.UTF8.GetBytes("hello"), "text")); Assert.True(LaraUI.DefaultApplication.TryGetNode("/a", out _)); } } [Fact] [Obsolete] public void LaraUiDefaultPage() { lock (_MyLock) { LaraUI.Publish("/b", () => new MyPage()); Assert.True(LaraUI.DefaultApplication.TryGetNode("/b", out _)); LaraUI.UnPublish("/b"); Assert.False(LaraUI.DefaultApplication.TryGetNode("/b", out _)); } } [Fact] [Obsolete] public void LaraUiDefaultService() { lock (_MyLock) { LaraUI.Publish(new WebComponentOptions { ComponentTagName = "lala-lala", ComponentType = typeof(RemovableComponent) }); Assert.True(LaraUI.DefaultApplication.TryGetComponent("lala-lala", out _)); LaraUI.UnPublishWebComponent("lala-lala"); Assert.False(LaraUI.DefaultApplication.TryGetComponent("lala-lala", out _)); } } [Fact] public void NoCurrentSessionParameters() { var inner = new InvalidOperationException("x"); var x = new NoCurrentSessionException("a", inner); Assert.Same(inner, x.InnerException); Assert.Equal("a", x.Message); } [Fact] public void NoCurrentSessionBase() { var x = new NoCurrentSessionException(); Assert.Null(x.InnerException); } [Fact] public void LaraUiDocument() { var x = new Mock(); var doc = new Document(new MyPage(), 100); x.Setup(x1 => x1.Document).Returns(doc); LaraUI.InternalContext.Value = x.Object; Assert.Same(doc, LaraUI.Document); Assert.Null(LaraUI.GetContextDocument(null)); } [Fact] public async Task SingleComponentPageTest() { var x = new Mock(); var doc = new Document(new MyPage(), 100); x.Setup(x1 => x1.Document).Returns(doc); LaraUI.InternalContext.Value = x.Object; var page = new SingleElementPage(() => new HtmlAnchorElement()); await page.OnGet(); Assert.Equal(1, doc.Body.ChildCount); var child = doc.Body.GetChildAt(0); Assert.NotNull(child as HtmlAnchorElement); } [Fact] public void SetExtraData() { var app = new Application(); var http = new Mock(); using var connections = new Connections(); var connection = connections.CreateConnection(IPAddress.Loopback); var x = new PageContext(app, http.Object, connection); x.SetExtraData("abc"); Assert.Equal("abc", x.JSBridge.EventData); Assert.Same(connection.Session, x.Session); } [Fact] public async void StopStops() { var counter = 0; using var x = new Application(); var host = new Mock(); x.SetHost(host.Object); var token = CancellationToken.None; host.Setup(x1 => x1.StopAsync(token)).Callback(() => counter++); await x.Stop(token); Assert.Equal(1, counter); } [Fact] public void ReuseConnection() { var app = Context.Application; var http = new Mock(); var request = new Mock(); var cookies = new Mock(); http.Setup(x => x.Request).Returns(request.Object); request.Setup(x => x.Cookies).Returns(cookies.Object); var info = new Mock(); http.Setup(x => x.Connection).Returns(info.Object); info.Setup(x => x.RemoteIpAddress).Returns(IPAddress.Loopback); app.CreateModeController(ApplicationMode.Default); var current = app.CreateConnection(IPAddress.Loopback); var id = current.Id.ToString(GlobalConstants.GuidFormat, CultureInfo.InvariantCulture); cookies.Setup(x => x.TryGetValue(GlobalConstants.CookieSessionId, out id)).Returns(true); var connection = PagePublished.GetConnection(app, http.Object); Assert.Equal(current, connection); } [Fact] public async void PageStatusCodeReturned() { var page = new MyStatusPage(); var http = new Mock(); var response = new Mock(); var headers = new Mock(); var body = new Mock(); response.Setup(x => x.Headers).Returns(headers.Object); http.Setup(x => x.Response).Returns(response.Object); response.SetupProperty(x => x.StatusCode); response.Setup(x => x.Body).Returns(body.Object); var ok = await PagePublished.RunPage(Context.Application, http.Object, page, new LaraOptions()); Assert.False(ok); Assert.Equal((int)HttpStatusCode.Unauthorized, http.Object.Response.StatusCode); } [Fact] public void ETagFormatCorrect() { const int hash = 12345; var expected = "\"" + hash.ToString(CultureInfo.InvariantCulture) + "\""; var etag = StaticContent.FormatETag(hash); Assert.Equal(expected, etag); } } } ================================================ FILE: src/Tests/Middleware/ServerEventsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Moq; using System; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Middleware { public class ServerEventsTesting : DummyContextTesting { private readonly Document _document; private readonly ServerEventsController _controller; private readonly Mock _socket; public ServerEventsTesting() { var page = new MyPage(); _document = new Document(page, BaseModeController.DefaultKeepAliveInterval); _controller = _document.GetServerEventsController(); _controller.ServerEventsOn(); _socket = new Mock(); } private async Task Initialize() { await _controller.GetSocketCompletion(_socket.Object); } [Fact] public async void DiscardSocketDiscards() { await Initialize(); await _controller.ServerEventsOff(); Assert.Equal(ServerEventsStatus.Disabled, _controller.ServerEventsStatus); } [Fact] public async void ServerEventRemainsOpen() { await Initialize(); Assert.True(_controller.SocketRemainsOpen(GlobalConstants.ServerSideEvent)); } [Fact] public async void ServerEventFlushFlushes() { await Initialize(); _document.OpenEventQueue(); _document.Body.AppendText("hello"); Assert.NotEmpty(_document.GetQueue()); await _controller.ServerEventFlush(); Assert.Empty(_document.GetQueue()); } [Fact] public async void FlushWhenDisabledRejected() { await Initialize(); await _document.ServerEventsOff(); _document.OpenEventQueue(); _document.Body.AppendText("hi"); await _controller.ServerEventsOff(); var found = false; try { await _controller.ServerEventFlush(); } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Fact] public async void NoFlushWhenQueueEmpty() { await Initialize(); _document.OpenEventQueue(); await _controller.ServerEventFlush(); _socket.Verify(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async void NoFlushWhenWaitingConnection() { await Initialize(); await _controller.ServerEventsOff(); _controller.ServerEventsOn(); _document.OpenEventQueue(); _document.Body.AppendText("hi"); _controller.ServerEventsOn(); await _controller.ServerEventFlush(); _socket.Verify(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async void AcceptingSocketFlushesPending() { _document.OpenEventQueue(); _document.Body.AppendText("hello"); _controller.ServerEventsOn(); await _controller.ServerEventFlush(); Assert.NotEmpty(_document.GetQueue()); await _document.GetSocketCompletion(_socket.Object); Assert.Empty(_document.GetQueue()); } [Fact] public async void ServerEventFlushes() { _document.OpenEventQueue(); _controller.ServerEventsOn(); await _controller.GetSocketCompletion(_socket.Object); using (_document.StartServerEvent()) { _document.Body.AppendText("hello"); Assert.NotEmpty(_document.GetQueue()); } Assert.Empty(_document.GetQueue()); } [Fact] public async void ServerEventsFlushesPartialChanges() { _document.OpenEventQueue(); _controller.ServerEventsOn(); await _controller.GetSocketCompletion(_socket.Object); using var access = _document.StartServerEvent(); _document.Body.AppendText("hello"); Assert.NotEmpty(_document.GetQueue()); await access.FlushPartialChanges(); Assert.Empty(_document.GetQueue()); } [Fact] public async void ServerEventFailDisposed() { _document.OpenEventQueue(); _controller.ServerEventsOn(); await _controller.GetSocketCompletion(_socket.Object); var access = _document.StartServerEvent(); access.Dispose(); var found = false; try { await access.FlushPartialChanges(); } catch (InvalidOperationException) { found = true; } Assert.True(found); } } } ================================================ FILE: src/Tests/Middleware/ToolsTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 9/2019 Author: Pablo Carbonell */ using Integrative.Lara.Tests.Main; using Moq; using System; using System.Net; using Xunit; namespace Integrative.Lara.Tests.Middleware { public class ToolsTesting : DummyContextTesting { [Fact] public void DocumentLocalException() { #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. LaraUI.InternalContext.Value = null; // on purpose for testing purposes #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. var error = false; try { // ReSharper disable once ObjectCreationAsStatement #pragma warning disable CA1806 // Do not ignore method results new DocumentLocal { Value = 5 }; #pragma warning restore CA1806 // Do not ignore method results } catch (InvalidOperationException) { error = true; } Assert.True(error); } [Fact] public void SessionLocalException() { #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. LaraUI.InternalContext.Value = null; #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. var error = false; try { // ReSharper disable once ObjectCreationAsStatement #pragma warning disable CA1806 // Do not ignore method results new SessionLocal { Value = 5 }; #pragma warning restore CA1806 // Do not ignore method results } catch (InvalidOperationException) { error = true; } Assert.True(error); } [Fact] public void DocumentLocalDefaultValue() { var local = BuildLocal(); Assert.Equal(0, local.Value); } [Fact] public void DocumentLocalWrites() { var local = BuildLocal(); local.Value = 3; Assert.Equal(3, local.Value); } [Fact] public void DocumentLocalSkipsReplacement() { var local = BuildLocal(); local.Value = 6; local.Value = 6; Assert.Equal(6, local.Value); } [Fact] public void DocumentLocalReplaces() { var local = BuildLocal(); local.Value = 7; local.Value = 8; Assert.Equal(8, local.Value); } [Fact] public async void DocumentLocalUnloads() { var local = BuildLocal(out var document); local.Value = 5; await document.NotifyUnload(); Assert.Equal(0, local.Value); } private static DocumentLocal BuildLocal() { return BuildLocal(out _); } private static DocumentLocal BuildLocal(out Document document) { var context = new Mock(); document = new Document(new MyPage(), BaseModeController.DefaultKeepAliveInterval); context.Setup(x => x.Document).Returns(document); LaraUI.InternalContext.Value = context.Object; return new DocumentLocal(); } private static SessionLocal GetSessionLocal(out Session session) { var context = new Mock(); var connection = new Connection(Guid.Parse("{B064124D-154D-4F49-89CF-CFC117509807}"), IPAddress.Loopback); session = new Session(connection); context.Setup(x => x.Session).Returns(session); LaraUI.InternalContext.Value = context.Object; return new SessionLocal(); } private static SessionLocal GetSessionLocal() { return GetSessionLocal(out _); } [Fact] public void SessionLocalDefaultValue() { var local = GetSessionLocal(); Assert.Equal(0, local.Value); } [Fact] public void SessionLocalWrites() { var local = GetSessionLocal(); local.Value = 3; Assert.Equal(3, local.Value); } [Fact] public void SessionLocalSkipsReplacement() { var local = GetSessionLocal(); local.Value = 6; local.Value = 6; Assert.Equal(6, local.Value); } [Fact] public void SessionLocalReplaces() { var local = GetSessionLocal(); local.Value = 7; local.Value = 8; Assert.Equal(8, local.Value); } [Fact] public void SessionLocalUnloads() { var local = GetSessionLocal(out var session); local.Value = 5; session.Close(); Assert.Equal(0, local.Value); } [Fact] public void SessionLocalService() { var context = new Mock(); var connection = new Connection(Guid.Parse("{B064124D-154D-4F49-89CF-CFC117509807}"), IPAddress.Loopback); Session? session = new Session(connection); context.Setup(x => x.TryGetSession(out session)).Returns(true); LaraUI.InternalContext.Value = context.Object; var local = new SessionLocal(); Assert.Equal(0, local.Value); local.Value = 11; Assert.Equal(11, local.Value); } [Fact] public void SmaValueNullTrue() { Assert.True(LaraTools.SameValue((string?) null, null)); } [Fact] [Obsolete("Old methods")] public void LaraFlushEvent() { var x = new HtmlInputElement(); var builder = new LaraBuilder(x); builder.FlushEvent("click"); x.NotifyEvent("click"); Assert.True(x.Events.TryGetValue("click", out var ev)); Assert.Equal("click", ev!.EventName); } } } ================================================ FILE: src/Tests/Middleware/WebServicesTesting.cs ================================================ /* Copyright (c) 2019-2021 Integrative Software LLC Created: 8/2019 Author: Pablo Carbonell */ using System; using System.Net; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using Xunit; namespace Integrative.Lara.Tests.Middleware { internal class RemovablePage : IPage { public Task OnGet() => Task.CompletedTask; } internal class RemovableComponent : WebComponent { public RemovableComponent() : base("x-removable") { } } internal class RemovableService : IWebService { public Task Execute() => Task.FromResult(string.Empty); } internal class MyBinary : IBinaryService { public Task Execute() { throw new NotImplementedException(); } } public class WebServicesTesting : DummyContextTesting { [Fact] public void LaraJsonSerializeType() { var tool = new LaraJson(); var data = new MyData { Counter = 5 }; var json = tool.Stringify(data, typeof(MyData)); var back = tool.Parse(json); Assert.NotNull(back); Assert.Equal(data.Counter, back!.Counter); } [DataContract] private class MyData { [DataMember] public int Counter { get; set; } } [Fact] public void TryParseCatchesSerializationErrors() { var tool = new LaraJson(); var ok = tool.TryParse("caca", out _); Assert.False(ok); } [Fact] public void ParseThrowsBadRequestException() { var tool = new LaraJson(); var found = false; try { tool.Parse("caca"); } catch (StatusCodeException e) { found = true; Assert.Equal(HttpStatusCode.BadRequest, e.StatusCode); } Assert.True(found); } [Fact] public void StatusCodeExceptionDefaultCode() { var e = new StatusCodeException(); Assert.Equal(HttpStatusCode.InternalServerError, e.StatusCode); } [Fact] public void StatusCodeExceptionMessageConstructor() { var e = new StatusCodeException("hello"); Assert.Equal("hello", e.Message); } [Fact] public void StatusCodeExceptionCodeAndMessage() { var e = new StatusCodeException(HttpStatusCode.Conflict, "lala"); Assert.Equal(HttpStatusCode.Conflict, e.StatusCode); Assert.Equal("lala", e.Message); } [Fact] public void StatusForbiddenExceptionDefault() { var e = new StatusForbiddenException(); Assert.Equal(HttpStatusCode.Forbidden, e.StatusCode); } [Fact] public void StatusForbiddenMessage() { var e = new StatusForbiddenException("lala"); Assert.Equal("lala", e.Message); Assert.Equal(HttpStatusCode.Forbidden, e.StatusCode); } [Fact] public void StatusForbiddenMessageInner() { var inner = new InvalidDataContractException(); var e = new StatusForbiddenException("lala", inner); Assert.Equal("lala", e.Message); Assert.Equal(HttpStatusCode.Forbidden, e.StatusCode); Assert.Same(inner, e.InnerException); } [Fact] public void LaraWebServiceAttributeDefaults() { var x = new LaraWebServiceAttribute(); Assert.Equal("POST", x.Method); Assert.Equal("application/json", x.ContentType); } [Fact] public void LaraWebServiceAttributeProperties() { var x = new LaraWebServiceAttribute { Address = "/", ContentType = "lala", Method = "PUT" }; Assert.Equal("/", x.Address); Assert.Equal("lala", x.ContentType); Assert.Equal("PUT", x.Method); } [LaraWebServiceAttribute(Address = "/myWS")] private class MyWebService : IWebService { public Task Execute() { return Task.FromResult(string.Empty); } } [LaraPageAttribute("/myPage")] private class MyPage : IPage { public Task OnGet() { return Task.CompletedTask; } } [Fact] public void PublishAssembliesService() { const string address = "/mydummy1"; using var app = new Application(); app.PublishService(new WebServiceContent { Address = address, Method = "POST", Factory = () => new DummyWs() }); var combined = Published.CombinePathMethod(address, "POST"); var found = app.TryGetNode(combined, out var item); Assert.True(found); var service = item as WebServicePublished; Assert.NotNull(service); Assert.NotNull(service!.Factory()); } private class DummyWs : IWebService { public Task Execute() => Task.FromResult(string.Empty); } [Fact] public void PublishAssembliesBinaryService() { const string address = "/mydummy2"; using var app = new Application(); app.PublishService(new BinaryServiceContent { Address = address, Method = "POST", Factory = () => new DummyBinaryWs() }); var combined = Published.CombinePathMethod(address, "POST"); var found = app.TryGetNode(combined, out var item); Assert.True(found); var service = item as BinaryServicePublished; Assert.NotNull(service); Assert.NotNull(service!.Factory()); } private class DummyBinaryWs : IBinaryService { public Task Execute() => Task.FromResult(Array.Empty()); } [Fact] public void PublishAssembliesPage() { const string address = "/mypapapapa"; using var app = new Application(); app.PublishPage(address, () => new MyPage()); var found = app.TryGetNode(address, out var item); Assert.True(found); var page = item as PagePublished; Assert.NotNull(page); Assert.NotNull(page!.CreateInstance()); } [Fact] [Obsolete] public void UnpublishWebservice() { const string address = "/mylalala"; using var app = new Application(); LaraUI.Publish(new WebServiceContent { Address = address, Factory = () => new MyWebService() }); LaraUI.UnPublish("/mylalala", "POST"); var combined = Published.CombinePathMethod(address, "POST"); Assert.False(app.TryGetNode(combined, out _)); } [Fact] public void VerifyTypeException() { var found = false; try { AssembliesReader.VerifyType(typeof(MyPage), "a", typeof(Element)); } catch (InvalidOperationException) { found = true; } Assert.True(found); } [Fact] public void ClearAllRemovesPublished() { using var app = new Application(); app.PublishPage(_pageName, () => new RemovablePage()); app.PublishService(new WebServiceContent { Address = _serviceName, Factory = () => new RemovableService(), Method = "GET" }); var bytes = Encoding.UTF8.GetBytes("hello"); app.PublishFile(_fileName, new StaticContent(bytes)); app.PublishComponent(new WebComponentOptions { ComponentTagName = _componentName, ComponentType = typeof(RemovableComponent) }); VerifyFound(app, true); app.ClearAllPublished(); VerifyFound(app, false); app.PublishAssemblies(); } private readonly string _serviceName = "/removableService" + GetRandom(); private readonly string _componentName = "x-removable" + GetRandom(); private readonly string _pageName = "/removablePage" + GetRandom(); private readonly string _fileName = "/removableFile" + GetRandom(); private static string GetRandom() { var random = new Random(); return random.Next(0, int.MaxValue).ToString(); } // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private void VerifyFound(Application app, bool found) { Assert.Equal(found, app.TryGetNode(_pageName, out _)); Assert.Equal(found, app.TryGetNode(_fileName, out _)); Assert.Equal(found, app.TryGetNode(_serviceName, out _)); Assert.Equal(found, app.TryGetComponent(_componentName, out _)); } [Fact] public void BinaryServiceContentProperties() { var x = new BinaryServiceContent { Address = "a", ContentType = "b", Method = "c", Factory = () => new MyBinary() }; Assert.Equal("a", x.Address); Assert.Equal("b", x.ContentType); Assert.Equal("c", x.Method); var result = x.Factory() as MyBinary; Assert.NotNull(result); } [Fact] public void BinaryServiceAttribute() { var x = new LaraBinaryServiceAttribute { Address = "a", ContentType = "b", Method = "c" }; Assert.Equal("a", x.Address); Assert.Equal("b", x.ContentType); Assert.Equal("c", x.Method); } } } ================================================ FILE: src/Tests/Tests.csproj ================================================  net5.0 false Integrative.Lara.Tests x64;AnyCPU latest enable all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive