├── .dockerignore ├── .github └── funding.yml ├── .gitignore ├── Dockerfile ├── DotNetify.Blazor.sln ├── README.md ├── Source ├── ClientConfiguration.cs ├── DotNetify.Blazor.csproj ├── ElementEventArgs.cs ├── ExceptionEventArgs.cs ├── IStylesheet.cs ├── IVMProxy.cs ├── IVMState.cs ├── JSCallback.cs ├── JSInterop.cs ├── LICENSE.md ├── Properties │ └── dotnetify.png ├── ServiceExtensions.cs ├── StyleSheet.razor ├── StylesheetLoader.cs ├── TypeProxy.cs ├── VMConnectOptions.cs ├── VMContext.razor ├── VMProxy.cs ├── bundleconfig.json └── wwwroot │ ├── dotnetify-blazor.js │ ├── dotnetify-blazor.min.js │ └── dotnetify.png ├── Templates ├── Basic.sln └── Basic │ ├── Client │ ├── App.razor │ ├── Basic.Client.csproj │ ├── Pages │ │ ├── Counter.razor │ │ ├── FetchData.razor │ │ ├── HelloWorld.razor │ │ └── Index.razor │ ├── Program.cs │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── NavMenu.razor │ │ └── SurveyPrompt.razor │ ├── _Imports.razor │ └── wwwroot │ │ ├── css │ │ ├── app.css │ │ ├── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ └── open-iconic │ │ │ ├── FONT-LICENSE │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ │ ├── favicon.ico │ │ └── index.html │ ├── Server │ ├── Basic.Server.csproj │ ├── Program.cs │ ├── Services │ │ └── WeatherForecastService.cs │ ├── Startup.cs │ ├── ViewModels │ │ ├── Counter.cs │ │ ├── FetchData.cs │ │ └── HelloWorld.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── Shared │ ├── Basic.Shared.csproj │ ├── ICounterState.cs │ ├── IFetchDataState.cs │ ├── IHelloWorldState.cs │ ├── Person.cs │ └── WeatherForecast.cs ├── UnitTests ├── TypeProxyTest.cs └── UnitTests.csproj ├── Website ├── Website.Client │ ├── App.razor │ ├── Pages │ │ ├── 1_Overview │ │ │ ├── AWSIntegration.razor │ │ │ ├── Basics.razor │ │ │ ├── DataFlow.razor │ │ │ ├── GetStarted.razor │ │ │ ├── Index.razor │ │ │ ├── Reactive.razor │ │ │ ├── RealtimePostgres.razor │ │ │ ├── Scaleout.razor │ │ │ └── Snippets │ │ │ │ ├── Details.razor │ │ │ │ ├── Master.razor │ │ │ │ ├── MasterDetails.razor │ │ │ │ ├── RealtimePush.razor │ │ │ │ └── ServerUpdate.razor │ │ ├── 2_Examples │ │ │ ├── ChatRoom.razor │ │ │ ├── Chatroom.razor.scss │ │ │ ├── CompositeView │ │ │ │ ├── CompositeView.razor │ │ │ │ ├── CompositeView.razor.scss │ │ │ │ ├── MovieDetails.razor │ │ │ │ ├── MovieFilter.razor │ │ │ │ └── MovieTable.razor │ │ │ ├── ControlTypes.razor │ │ │ ├── ControlTypes.razor.scss │ │ │ ├── Dashboard │ │ │ │ ├── ActivitiesCard.razor │ │ │ │ ├── ActivitiesCard.razor.scss │ │ │ │ ├── Dashboard.razor │ │ │ │ ├── InfoCard.razor │ │ │ │ └── InfoCard.razor.scss │ │ │ ├── Form │ │ │ │ ├── AddressForm.razor │ │ │ │ ├── Form.razor │ │ │ │ ├── FormTabs.razor │ │ │ │ ├── NewFormDialog.razor │ │ │ │ ├── PersonForm.razor │ │ │ │ └── PhoneForm.razor │ │ │ ├── HelloWorld.razor │ │ │ ├── HelloWorld.razor.scss │ │ │ ├── SecurePage │ │ │ │ ├── AdminSecurePage.razor │ │ │ │ ├── Login.razor │ │ │ │ ├── Login.razor.scss │ │ │ │ └── SecurePage.razor │ │ │ ├── SimpleList.razor │ │ │ └── SimpleList.razor.scss │ │ ├── 3_APIReferences │ │ │ ├── CRUD.razor │ │ │ ├── DI.razor │ │ │ ├── DotNetClient.razor │ │ │ ├── Filter.razor │ │ │ ├── Middleware.razor │ │ │ ├── Multicast.razor │ │ │ ├── ScopedCss.razor │ │ │ ├── Security.razor │ │ │ └── WebApiMode.razor │ │ ├── 4_Premium │ │ │ ├── DotNetifyLoadTester.razor │ │ │ ├── DotNetifyObserver.razor │ │ │ ├── DotNetifyResiliencyAddon.razor │ │ │ └── DotNetifyTesting.razor │ │ └── _Imports.razor │ ├── Program.cs │ ├── Properties │ │ ├── Resources.Designer.cs │ │ └── Resources.resx │ ├── Shared │ │ ├── ArticleLayout.razor │ │ ├── ArticleLayout.razor.scss │ │ ├── ExampleLayout.razor │ │ ├── Expander.razor │ │ ├── Expander.razor.scss │ │ ├── GithubLink.razor │ │ ├── GithubLink.razor.scss │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.scss │ │ ├── MenuLinks.razor │ │ ├── MenuLinks.razor.scss │ │ ├── SelectFramework.razor │ │ ├── SelectFramework.razor.scss │ │ ├── TwitterLink.razor │ │ └── TwitterLink.razor.scss │ ├── Website.Client.csproj │ ├── _Imports.razor │ └── wwwroot │ │ ├── app.js │ │ ├── assets │ │ ├── AWSIntegration.svg │ │ ├── ScopedVMPattern.svg │ │ ├── SiloedVMPattern.svg │ │ └── SingleVMPattern.svg │ │ ├── css │ │ ├── app.css │ │ ├── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ └── prism.css │ │ ├── dotnetify-elements.bundle.js │ │ ├── dotnetify-react.min.js │ │ ├── favicon.ico │ │ └── index.html └── Website.Server │ ├── AuthServer.cs │ ├── Docs │ ├── CRUD.md │ ├── DI.md │ ├── DotNetClient.md │ ├── Examples │ │ ├── ChatRoom.md │ │ ├── CompositeView.md │ │ ├── ControlTypes.md │ │ ├── Dashboard.md │ │ ├── Form.md │ │ ├── HelloWorld.md │ │ ├── SecurePage.md │ │ └── SimpleList.md │ ├── Filter.md │ ├── Middleware.md │ ├── Multicast.md │ ├── Overview │ │ ├── AWSIntegration.md │ │ ├── Basics.md │ │ ├── DataFlow.md │ │ ├── GetStarted.md │ │ ├── Overview.md │ │ ├── Reactive.md │ │ ├── RealtimePostgres.md │ │ └── Scaleout.md │ ├── Premium │ │ ├── DotNetifyLoadTester.md │ │ ├── DotNetifyObserver.md │ │ ├── DotNetifyResiliencyAddon.md │ │ └── DotNetifyTesting.md │ ├── ScopedCss.md │ ├── Security.md │ └── WebApiMode.md │ ├── ExamplePipelines │ ├── ExtractAccessTokenMiddleware.cs │ └── SetAccessTokenFilter.cs │ ├── Program.cs │ ├── Properties │ ├── PublishProfiles │ │ └── FTPProfile.pubxml │ └── launchSettings.json │ ├── Services │ ├── AFITop100.json │ ├── EmployeeRepository.cs │ ├── MockLiveDataService.cs │ ├── MovieService.cs │ ├── WebStoreService.cs │ └── webstore.json │ ├── Startup.cs │ ├── ViewModels │ ├── Docs.cs │ ├── Examples │ │ ├── ChatRoom.cs │ │ ├── CompositeView │ │ │ ├── CompositeView.cs │ │ │ ├── FilterableMovieTableVM.cs │ │ │ ├── MovieDetailsVM.cs │ │ │ ├── MovieFilterVM.cs │ │ │ └── MovieTableVM.cs │ │ ├── ControlTypes.cs │ │ ├── HelloWorld.cs │ │ ├── LiveChart.cs │ │ ├── MasterDetails.cs │ │ ├── Overview.cs │ │ ├── SecurePage.cs │ │ └── SimpleList.cs │ ├── ExamplesWithElements │ │ ├── Dashboard.cs │ │ └── Form │ │ │ ├── AddressForm.cs │ │ │ ├── CustomerForm.cs │ │ │ ├── CustomerFormData.cs │ │ │ ├── NewCustomerForm.cs │ │ │ ├── PersonForm.cs │ │ │ ├── PhoneForm.cs │ │ │ └── Services │ │ │ ├── Customer.cs │ │ │ └── CustomerRepository.cs │ └── MainNav.cs │ ├── Website.Server.csproj │ ├── appsettings.json │ └── pulse-ui │ ├── index.html │ ├── section.html │ ├── section_template.html │ └── style.css └── deploy.bat /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dsuryd] 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 2 | LABEL stage=build 3 | WORKDIR /src 4 | COPY ./Source ./Source 5 | COPY ./Website ./Website 6 | 7 | RUN dotnet publish ./Website/Website.Server/Website.Server.csproj -c Release -o /app 8 | 9 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 10 | WORKDIR /app 11 | COPY --from=build /app . 12 | ARG aspnetenv=Production 13 | 14 | ENV ASPNETCORE_ENVIRONMENT ${aspnetenv} 15 | CMD ASPNETCORE_URLS=http://*:$PORT dotnet Website.Server.dll 16 | -------------------------------------------------------------------------------- /DotNetify.Blazor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetify.Blazor", "Source\DotNetify.Blazor.csproj", "{CDF41D3D-4540-4C0B-810E-0B6AAEB7C7E2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Website.Client", "Website\Website.Client\Website.Client.csproj", "{FA792C80-6B46-4FF0-9152-517A58F9E6E6}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Website.Server", "Website\Website.Server\Website.Server.csproj", "{0EE42035-815F-4A38-84B9-76C10FB35245}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{62C2DA59-FBE5-453B-ACB9-8FC907A6FE91}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {CDF41D3D-4540-4C0B-810E-0B6AAEB7C7E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {CDF41D3D-4540-4C0B-810E-0B6AAEB7C7E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {CDF41D3D-4540-4C0B-810E-0B6AAEB7C7E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {CDF41D3D-4540-4C0B-810E-0B6AAEB7C7E2}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {FA792C80-6B46-4FF0-9152-517A58F9E6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {FA792C80-6B46-4FF0-9152-517A58F9E6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {FA792C80-6B46-4FF0-9152-517A58F9E6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {FA792C80-6B46-4FF0-9152-517A58F9E6E6}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {0EE42035-815F-4A38-84B9-76C10FB35245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {0EE42035-815F-4A38-84B9-76C10FB35245}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {0EE42035-815F-4A38-84B9-76C10FB35245}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {0EE42035-815F-4A38-84B9-76C10FB35245}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {62C2DA59-FBE5-453B-ACB9-8FC907A6FE91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {62C2DA59-FBE5-453B-ACB9-8FC907A6FE91}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {62C2DA59-FBE5-453B-ACB9-8FC907A6FE91}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {62C2DA59-FBE5-453B-ACB9-8FC907A6FE91}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {370057FB-033F-4D2E-A912-5CA47F7696A2} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | [![NuGet version](https://badge.fury.io/nu/DotNetify.Blazor.svg)](https://badge.fury.io/nu/DotNetify.Blazor) 4 | 5 | **DotNetify-Blazor** is a free, open source project that lets you create real-time, reactive, cross-platform apps with Blazor WebAssembly. 6 | 7 | ## Features 8 | 9 | - Server-side view model: don't let your client download too much code; keep most processing on the back-end and only send things that change. 10 | - Declarative state hydration: eliminate the need to write data-fetching boilerplate services. Send data to the back-end by simply invoking an interface method. 11 | - Simple real-time abstraction: push data to client in real-time from multiple classes with no coupling to low-level SignalR details. 12 | - **Can switch to Web API**: don't need real-time/SignalR? Keep your view model stateless and switch to Web API endpoint instead. 13 | - **Scoped CSS**: built-in support for SASS-like syntax and replacing styles at runtime! 14 | - **[Web Components](https://dotnetify.net/elements?webcomponent)**: comes with a library of HTML native web components to implement layouts, online forms, charts, and more --- all supporting CSS isolation. Usage is optional! 15 | - **Reusable with Javascript SPAs**: Can't always use Blazor? The same view models you write for Blazor can be reused with Javascript UI frameworks without change. DotNetify has full support for React and Vue, and can be made to work with Angular and others. 16 | - **Reusable with .NET desktop clients**: reuse the same view models with .NET-based client apps (WPF/Avalonia). 17 | - **Multicasting**: send real-time data to multiple clients at once; perfect for real-time collaboration/data sync. 18 | - **Reactive**: make your view model declarative with streaming, observable properties + asynch programming. 19 | - **Dependency injection**: inject dependency objects through the class constructor. 20 | - **Middlewares/filters**: build a pipeline to do all sorts of things before reaching the view models. 21 | - **Bearer token authentication**: pass authentication header as payload instead of query string. 22 | 23 | ## Install 24 | 25 | ``` 26 | dotnet add package DotNetify.Blazor 27 | ``` 28 | 29 | ## Documentation 30 | 31 | Documentation and live demo can be found at [https://dotnetify.net/blazor](https://dotnetify.net/blazor). 32 | 33 | DotNetify core repo: [https://github.com/dsuryd/dotNetify](https://github.com/dsuryd/dotNetify). 34 | 35 | ## License 36 | 37 | Licensed under the Apache License, Version 2.0. 38 | 39 | ## Contributing 40 | 41 | All contribution is welcome: star this project, let others know about it, report issues, submit pull requests! 42 | 43 | _Logo design by [area55git](https://github.com/area55git)._ 44 | -------------------------------------------------------------------------------- /Source/ClientConfiguration.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System.Reflection; 18 | using System.Text.Json.Serialization; 19 | 20 | namespace DotNetify.Blazor 21 | { 22 | public class ClientConfiguration 23 | { 24 | /// 25 | /// Enable debug output on the browser's developer console. 26 | /// 27 | public bool Debug { get; set; } 28 | 29 | /// 30 | /// DotNetify hub server URL. 31 | /// 32 | public string HubServerUrl { get; set; } 33 | 34 | /// 35 | /// Assemblies to look for StyleSheet files. 36 | /// 37 | [JsonIgnore] 38 | public Assembly[] StyleSheetAssemblies { get; set; } 39 | 40 | /// 41 | /// Web socket server URL (ws://). 42 | /// 43 | public string WebSocketServerUrl { get; set; } 44 | } 45 | } -------------------------------------------------------------------------------- /Source/DotNetify.Blazor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | true 6 | 8.0 7 | 3.0 8 | true 9 | DotNetify.Blazor 10 | 2.1.2 11 | DotNetify.Blazor 12 | Dicky Suryadi 13 | DotNetify.Blazor 14 | Real-time, reactive MVVM library for Blazor web assembly apps. 15 | Copyright 2019-2022 16 | https://dotnetify.net 17 | https://github.com/dsuryd/dotNetify-Blazor 18 | LICENSE.md 19 | true 20 | dotnetify.png 21 | 22 | 23 | 24 | DEBUG;TRACE 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Source/ElementEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | namespace DotNetify.Blazor 18 | { 19 | public class ElementEventArgs 20 | { 21 | /// 22 | /// Identifies the DOM element raising the event. 23 | /// 24 | public string TargetId { get; set; } 25 | 26 | /// 27 | /// Event name. 28 | /// 29 | public string EventName { get; set; } 30 | 31 | /// 32 | /// Event arguments. 33 | /// 34 | public object EventArgs { get; set; } 35 | } 36 | } -------------------------------------------------------------------------------- /Source/ExceptionEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | namespace DotNetify.Blazor 18 | { 19 | public class ExceptionEventArgs 20 | { 21 | /// 22 | /// Exception name. 23 | /// 24 | public string Name { get; set; } 25 | 26 | /// 27 | /// Exception message. 28 | /// 29 | public string Message { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /Source/IStylesheet.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | namespace DotNetify.Blazor 18 | { 19 | public interface IStyleSheet 20 | { 21 | /// 22 | /// Gets the style sheet content of an embedded resource. It doesn't need the full name, only a sufficiently unique substring. 23 | /// 24 | /// Name of the embedded resource containing a style sheet. 25 | string this[string embeddedResourceName] { get; } 26 | } 27 | } -------------------------------------------------------------------------------- /Source/IVMProxy.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | using System.Collections.Generic; 19 | using System.Threading.Tasks; 20 | using Microsoft.AspNetCore.Components; 21 | 22 | namespace DotNetify.Blazor 23 | { 24 | public interface IVMProxy : IDisposable 25 | { 26 | /// 27 | /// Reference to the associated 'd-vm-context' HTML markup. 28 | /// 29 | ElementReference ElementRef { get; set; } 30 | 31 | /// 32 | /// Listens to the state change event from the server-side view model. 33 | /// 34 | /// Gets called when the client receives state change from the server-side view model. 35 | Task HandleStateChangeAsync(Action stateChangeEventCallback); 36 | 37 | /// 38 | /// Listens to the events from the web component elements under this VM context. 39 | /// 40 | /// Gets called when an element under this VM context raises an event. 41 | Task HandleElementEventAsync(Action eventCallback); 42 | 43 | /// 44 | /// Listens to the exception event from the server-side view model. 45 | /// 46 | /// Gets called when the client receives exception from the server-side view model. 47 | Task HandleExceptionAsync(Action exceptionEventCallback); 48 | 49 | /// 50 | /// Listens to an event from a DOM element. 51 | /// 52 | /// Event argument type. 53 | /// Document element. 54 | /// Event name. 55 | /// Event callback. 56 | Task HandleDomEventAsync(string eventName, ElementReference domElement, Action eventCallback); 57 | 58 | /// 59 | /// Dispatches property value to server-side view model. 60 | /// 61 | /// Name that matches a server-side view model property. 62 | /// Value to be dispatched. 63 | Task DispatchAsync(string propertyName, object propertyValue = null); 64 | 65 | /// 66 | /// Dispatches multiple property values to server-side view model. 67 | /// 68 | /// Dictionary of property name and values. 69 | Task DispatchAsync(Dictionary properties); 70 | 71 | /// 72 | /// Disposes the context element. 73 | /// 74 | /// 75 | Task DisposeAsync(); 76 | } 77 | } -------------------------------------------------------------------------------- /Source/IVMState.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | 19 | namespace DotNetify.Blazor 20 | { 21 | /// 22 | /// Use this attribute on a property to make any changed value to be dispatched to the server view model. 23 | /// 24 | [AttributeUsage(AttributeTargets.Property)] 25 | public class WatchAttribute : Attribute 26 | { 27 | } 28 | 29 | /// 30 | /// Required interface for view model state that wants to use watch attributes. 31 | /// 32 | public interface IVMState 33 | { 34 | IVMProxy VMProxy { get; set; } 35 | } 36 | } -------------------------------------------------------------------------------- /Source/JSCallback.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using Microsoft.JSInterop; 18 | using System; 19 | 20 | namespace DotNetify.Blazor 21 | { 22 | public class JSCallback 23 | { 24 | private readonly Action _callback; 25 | 26 | public JSCallback(Action callback) 27 | { 28 | _callback = callback; 29 | } 30 | 31 | [JSInvokable] 32 | public string Callback(object arg) 33 | { 34 | _callback(arg); 35 | return string.Empty; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Source/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019-2022 Dicky Suryadi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | -------------------------------------------------------------------------------- /Source/Properties/dotnetify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Source/Properties/dotnetify.png -------------------------------------------------------------------------------- /Source/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | using System.Reflection; 19 | using Microsoft.Extensions.DependencyInjection; 20 | using Microsoft.JSInterop; 21 | 22 | namespace DotNetify.Blazor 23 | { 24 | public static class ServiceExtensions 25 | { 26 | /// 27 | /// Adds DotNetify-Blazor services to the service collection. 28 | /// 29 | public static IServiceCollection AddDotNetifyBlazor(this IServiceCollection services, Action options = null) 30 | { 31 | var config = new ClientConfiguration(); 32 | var styleSheetLoader = new StyleSheetLoader(); 33 | 34 | services.AddTransient(); 35 | services.AddSingleton(styleSheetLoader); 36 | 37 | options?.Invoke(config); 38 | 39 | // Load StyleSheet files from the specified assembly(ies). 40 | config.StyleSheetAssemblies ??= new Assembly[] { Assembly.GetCallingAssembly() }; 41 | foreach (var assembly in config.StyleSheetAssemblies) 42 | styleSheetLoader.Load(assembly); 43 | 44 | // Execute scripts to set dotNetify configuration. 45 | var jsInterop = new JSInterop(services.BuildServiceProvider().GetRequiredService()); 46 | _ = jsInterop.ConfigureDotNetifyAsync(config); 47 | 48 | return services; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Source/StyleSheet.razor: -------------------------------------------------------------------------------- 1 | @*Copyright 2020 Dicky Suryadi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*@ 14 | 15 | @inject IStyleSheet StyleSheetLoader; 16 | 17 | 18 | @ChildContent 19 | 20 | 21 | @code { 22 | /// 23 | /// Component where this StyleSheet component is in. Its name is used to located the embedded resource that contains the style sheet to use. 24 | /// 25 | [Parameter] public object Context { get; set; } 26 | 27 | /// 28 | /// Name of the embedded resource that contains the style sheet to use. 29 | /// 30 | [Parameter] public string Name { get; set; } 31 | 32 | /// 33 | /// Occurs after the style sheet is loaded, to provide an opportunity to modify it before being applied. 34 | /// 35 | [Parameter] public Func OnLoad { get; set; } 36 | 37 | [Parameter] public RenderFragment ChildContent { get; set; } 38 | 39 | private string css; 40 | 41 | protected override void OnInitialized() 42 | { 43 | string resourceName = !string.IsNullOrWhiteSpace(Name) ? Name : Context?.GetType().Name; 44 | if (!string.IsNullOrWhiteSpace(resourceName)) 45 | { 46 | this.css = StyleSheetLoader[resourceName]; 47 | if (OnLoad != null) 48 | this.css = OnLoad(this.css); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Source/StylesheetLoader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | using System.Collections.Generic; 19 | using System.IO; 20 | using System.Linq; 21 | using System.Reflection; 22 | using System.Text.RegularExpressions; 23 | 24 | namespace DotNetify.Blazor 25 | { 26 | internal class StyleSheetLoader : IStyleSheet 27 | { 28 | private readonly List> _styleSheets = new List>(); 29 | 30 | /// 31 | /// Gets the content of a style sheet embedded resource. It doesn't need the full name, only a sufficiently unique substring. 32 | /// 33 | /// Name of the embedded resource containing the style sheet. 34 | public string this[string embeddedResourceName] 35 | { 36 | get 37 | { 38 | var styleSheet = _styleSheets.FirstOrDefault(x => x.Key.Equals(embeddedResourceName, StringComparison.InvariantCultureIgnoreCase)); 39 | if (string.IsNullOrEmpty(styleSheet.Key)) 40 | throw new FileNotFoundException($"No embedded StyleSheet resource by the name of '{embeddedResourceName}'."); 41 | 42 | // Remove whitespaces, except those between words. 43 | return Regex.Replace(styleSheet.Value, @"(?!\b\s+\b)(?!\s\d)(?!\s[-\+])\s+", string.Empty).Replace(": ", ":"); 44 | } 45 | } 46 | 47 | /// 48 | /// Loads all style sheets in an assembly. 49 | /// 50 | internal void Load(Assembly assembly) 51 | { 52 | var styleSheets = assembly.GetManifestResourceNames() 53 | .Where(resource => resource.ToLower().EndsWith(".css") || resource.ToLower().EndsWith(".scss")) 54 | .Select(resourceName => 55 | { 56 | using Stream stream = assembly.GetManifestResourceStream(resourceName); 57 | using StreamReader reader = new StreamReader(stream); 58 | 59 | // Use the resource name excluding namespace and extensions for the lookup key. 60 | string pattern = resourceName.Contains(".razor.") ? @"(?:.+\.)+(.+)\.razor\.s?css" : @"(?:.+\.)+(.+)\.s?css"; 61 | var match = Regex.Match(resourceName, pattern); 62 | string key = match.Success && match.Groups.Count > 1 ? match.Groups[1].Value : resourceName; 63 | 64 | return new KeyValuePair(key, reader.ReadToEnd()); 65 | }); 66 | 67 | _styleSheets.AddRange(styleSheets); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Source/VMConnectOptions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Dicky Suryadi 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | using System.Collections.Generic; 19 | using Newtonsoft.Json; 20 | 21 | namespace DotNetify.Blazor 22 | { 23 | /// 24 | /// Configuration options that can be sent along with request to connect to a server-side view model. 25 | /// 26 | public class VMConnectOptions 27 | { 28 | /// 29 | /// Arguments to initialize the view model. 30 | /// 31 | public Dictionary VMArg { get; set; } 32 | 33 | /// 34 | /// Request headers. 35 | /// 36 | public Dictionary Headers { get; set; } 37 | 38 | /// 39 | /// Use HTTP Web API endpoint instead of SignalR hub. 40 | /// 41 | public bool WebApi { get; set; } 42 | 43 | /// 44 | /// Callback when getting exception from server-side view model. 45 | /// 46 | [JsonIgnore] 47 | public Action ExceptionHandler { get; set; } 48 | } 49 | } -------------------------------------------------------------------------------- /Source/VMContext.razor: -------------------------------------------------------------------------------- 1 | @*Copyright 2020 Dicky Suryadi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*@ 14 | 15 | @implements IDisposable 16 | @inject IVMProxy vm 17 | @typeparam TState 18 | 19 | 20 | @ChildContent 21 | 22 | 23 | @code { 24 | /// 25 | /// Name of the view model to connect with. 26 | /// 27 | [Parameter] public string VM { get; set; } 28 | 29 | /// 30 | /// Configuration options that can be sent along with request to connect to a server-side view model. 31 | /// 32 | [Parameter] public VMConnectOptions Options { get; set; } 33 | 34 | /// 35 | /// Enables web API mode. 36 | /// 37 | [Parameter] public bool WebApi { get; set; } 38 | 39 | /// 40 | /// Occurs on receiving state update from the server-side view model. 41 | /// 42 | [Parameter] public EventCallback OnStateChange { get; set; } 43 | 44 | /// 45 | /// Occurs on receiving a custom event from a DotNetify-Element's web component. 46 | /// 47 | [Parameter] public EventCallback OnElementEvent { get; set; } 48 | 49 | /// 50 | /// Occurs on receiving an exception from the server-side view model. 51 | /// 52 | [Parameter] public EventCallback OnException { get; set; } 53 | 54 | [Parameter] public RenderFragment ChildContent { get; set; } 55 | 56 | protected override void OnInitialized() 57 | { 58 | if (WebApi) 59 | { 60 | Options = Options ?? new VMConnectOptions(); 61 | Options.WebApi = true; 62 | } 63 | } 64 | 65 | protected override async Task OnAfterRenderAsync(bool firstRender) 66 | { 67 | if (firstRender) 68 | { 69 | await vm.HandleStateChangeAsync(state => OnStateChange.InvokeAsync(state)); 70 | await vm.HandleElementEventAsync(args => HandleElementEvent(args)); 71 | await vm.HandleExceptionAsync(ex => HandleException(ex)); 72 | } 73 | } 74 | 75 | public void Dispose() 76 | { 77 | vm.Dispose(); 78 | } 79 | 80 | public async Task DispatchAsync(string propertyName, object propertyValue = null) 81 | { 82 | await vm.DispatchAsync(propertyName, propertyValue); 83 | } 84 | 85 | public void HandleEvent(ElementReference elementRef, string eventName, Action eventHandler) 86 | { 87 | vm.HandleDomEventAsync(eventName, elementRef, e => eventHandler?.Invoke()); 88 | } 89 | 90 | public void HandleEvent(ElementReference elementRef, string eventName, Action eventHandler) 91 | { 92 | vm.HandleDomEventAsync(eventName, elementRef, e => eventHandler?.Invoke(e)); 93 | } 94 | 95 | private void HandleElementEvent(ElementEventArgs args) 96 | { 97 | OnElementEvent.InvokeAsync(args); 98 | } 99 | 100 | private void HandleException(ExceptionEventArgs args) 101 | { 102 | OnException.InvokeAsync(args); 103 | } 104 | } -------------------------------------------------------------------------------- /Source/bundleconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "outputFileName": "wwwroot/dotnetify-blazor.min.js", 4 | "inputFiles": [ 5 | "wwwroot/dotnetify-blazor.js" 6 | ] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /Source/wwwroot/dotnetify-blazor.js: -------------------------------------------------------------------------------- 1 | dotnetify_blazor = { 2 | version: 2.1, 3 | _eventListeners: [], 4 | addEventListener: function (event, elem, callbackHelper) { 5 | if (typeof elem === 'string') elem = document.querySelector(elem); 6 | if (!(elem && typeof elem.addEventListener === 'function')) throw "Cannot listen to event '" + event + "': invalid VMContext @ref."; 7 | 8 | // Blazor component inside React container must be temporarily moved out until the container is rendered, 9 | // otherwise it will re-rendered as React children and lose its association with the Blazor app. 10 | if (elem.getAttribute('vm') && event === 'onStateChange') { 11 | const insideContainer = () => { 12 | let parent = elem.parentElement; 13 | while (parent) { 14 | if (parent._isContainer) return parent; 15 | parent = parent.parentElement; 16 | } 17 | }; 18 | const parent = insideContainer(); 19 | if (parent) { 20 | const vmId = elem.getAttribute('vm'); 21 | const elemParent = elem.parentElement; 22 | const tempPlaceholderId = '__tmp_' + vmId; 23 | const tempPlaceholder = document.createElement('span'); 24 | const tempLocation = document.createElement('span'); 25 | const tempParent = document.body.appendChild(tempLocation); 26 | const restore = () => { 27 | const placeholder = document.getElementById(tempPlaceholderId); 28 | if (placeholder) { 29 | while (tempParent.childNodes.length > 0) placeholder.parentElement.appendChild(tempParent.childNodes[0]); 30 | document.body.removeChild(tempParent); 31 | placeholder.parentElement.removeChild(placeholder); 32 | } 33 | elem.setAttribute('vm', vmId); 34 | return true; 35 | }; 36 | 37 | tempPlaceholder.setAttribute('id', tempPlaceholderId); 38 | tempLocation.style.display = 'none'; 39 | elem.setAttribute('vm', ''); 40 | while (elemParent.childNodes.length > 0) tempParent.appendChild(elemParent.childNodes[0]); 41 | elemParent.appendChild(tempPlaceholder); 42 | 43 | let moved = false; 44 | const restorePosition = () => { 45 | moved = moved || restore(); 46 | parent.removeEventListener('mounted', restorePosition); 47 | }; 48 | parent.addEventListener('mounted', restorePosition); 49 | setTimeout(restorePosition, 750); 50 | } 51 | } 52 | 53 | const callback = e => { 54 | callbackHelper.invokeMethodAsync('Callback', JSON.stringify(e.detail, 55 | // Don't stringify DOM elements. 56 | (_, value) => value instanceof HTMLElement ? value.id : value 57 | )); 58 | }; 59 | if (!dotnetify_blazor._eventListeners.some(x => x.elem === elem && x.event === event)) { 60 | dotnetify_blazor._eventListeners.push({ elem, event, remove: () => elem.removeEventListener(event, callback) }); 61 | elem.addEventListener(event, callback); 62 | } 63 | }, 64 | configure: function (config) { 65 | dotnetify.debug = config.debug; 66 | if (config.hubServerUrl) dotnetify.hubServerUrl = config.hubServerUrl; 67 | if (config.webSocketServerUrl) dotnetify.hub = dotnetify.createWebSocketHub(config.webSocketServerUrl); 68 | }, 69 | dispatch: function (elem, state) { 70 | if (!(elem && elem.vm)) throw 'Invalid VMContext @ref.'; 71 | const stateJson = JSON.parse(state); 72 | Object.assign(elem.state, stateJson); 73 | elem.vm.$dispatch(stateJson); 74 | }, 75 | removeAllEventListeners: function (elem) { 76 | dotnetify_blazor._eventListeners.filter(x => x.elem === elem).forEach(x => x.remove()); 77 | dotnetify_blazor._eventListeners = dotnetify_blazor._eventListeners.filter(x => x.elem !== elem); 78 | } 79 | }; -------------------------------------------------------------------------------- /Source/wwwroot/dotnetify-blazor.min.js: -------------------------------------------------------------------------------- 1 | dotnetify_blazor={version:2.1,_eventListeners:[],addEventListener:function(n,t,i){if(typeof t=="string"&&(t=document.querySelector(t)),!(t&&typeof t.addEventListener=="function"))throw"Cannot listen to event '"+n+"': invalid VMContext @ref.";if(t.getAttribute("vm")&&n==="onStateChange"){const i=()=>{let n=t.parentElement;while(n){if(n._isContainer)return n;n=n.parentElement}},n=i();if(n){const f=t.getAttribute("vm"),r=t.parentElement,e="__tmp_"+f,o=document.createElement("span"),s=document.createElement("span"),i=document.body.appendChild(s),c=()=>{const n=document.getElementById(e);if(n){while(i.childNodes.length>0)n.parentElement.appendChild(i.childNodes[0]);document.body.removeChild(i);n.parentElement.removeChild(n)}return t.setAttribute("vm",f),!0};for(o.setAttribute("id",e),s.style.display="none",t.setAttribute("vm","");r.childNodes.length>0;)i.appendChild(r.childNodes[0]);r.appendChild(o);let h=!1;const u=()=>{h=h||c(),n.removeEventListener("mounted",u)};n.addEventListener("mounted",u);setTimeout(u,750)}}const r=n=>{i.invokeMethodAsync("Callback",JSON.stringify(n.detail,(n,t)=>t instanceof HTMLElement?t.id:t))};dotnetify_blazor._eventListeners.some(i=>i.elem===t&&i.event===n)||(dotnetify_blazor._eventListeners.push({elem:t,event:n,remove:()=>t.removeEventListener(n,r)}),t.addEventListener(n,r))},configure:function(n){dotnetify.debug=n.debug;n.hubServerUrl&&(dotnetify.hubServerUrl=n.hubServerUrl);n.webSocketServerUrl&&(dotnetify.hub=dotnetify.createWebSocketHub(n.webSocketServerUrl))},dispatch:function(n,t){if(!(n&&n.vm))throw"Invalid VMContext @ref.";const i=JSON.parse(t);Object.assign(n.state,i);n.vm.$dispatch(i)},removeAllEventListeners:function(n){dotnetify_blazor._eventListeners.filter(t=>t.elem===n).forEach(n=>n.remove());dotnetify_blazor._eventListeners=dotnetify_blazor._eventListeners.filter(t=>t.elem!==n)}}; -------------------------------------------------------------------------------- /Source/wwwroot/dotnetify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Source/wwwroot/dotnetify.png -------------------------------------------------------------------------------- /Templates/Basic.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30128.74 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basic.Server", "Basic\Server\Basic.Server.csproj", "{415C9E90-4080-4421-93A7-B56F282FCCAB}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basic.Client", "Basic\Client\Basic.Client.csproj", "{6B8C938E-3297-4215-83EA-EDFDEDACA0B0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basic.Shared", "Basic\Shared\Basic.Shared.csproj", "{2BF069AE-0E49-48C4-8B32-FFE745DE33C5}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {415C9E90-4080-4421-93A7-B56F282FCCAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {415C9E90-4080-4421-93A7-B56F282FCCAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {415C9E90-4080-4421-93A7-B56F282FCCAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {415C9E90-4080-4421-93A7-B56F282FCCAB}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {6B8C938E-3297-4215-83EA-EDFDEDACA0B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {6B8C938E-3297-4215-83EA-EDFDEDACA0B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {6B8C938E-3297-4215-83EA-EDFDEDACA0B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {6B8C938E-3297-4215-83EA-EDFDEDACA0B0}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {2BF069AE-0E49-48C4-8B32-FFE745DE33C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {2BF069AE-0E49-48C4-8B32-FFE745DE33C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {2BF069AE-0E49-48C4-8B32-FFE745DE33C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {2BF069AE-0E49-48C4-8B32-FFE745DE33C5}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {0B10CE16-55E3-4D2D-8724-984CF8F09FE3} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Templates/Basic/Client/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Basic.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 3.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 |

Counter

4 | 5 | 6 |

Current count: @state?.CurrentCount

7 | 8 | 9 |
10 | 11 | @code { 12 | private ICounterState state; 13 | 14 | private void UpdateState(ICounterState state) 15 | { 16 | this.state = state; 17 | StateHasChanged(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | @using Basic.Shared 3 | @inject HttpClient Http 4 | 5 |

Weather forecast

6 | 7 |

This component demonstrates fetching data from the server.

8 | 9 | 10 | @if (state == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in state.Forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 |
39 | 40 | @code { 41 | private IFetchDataState state; 42 | 43 | private void UpdateState(IFetchDataState state) 44 | { 45 | this.state = state; 46 | StateHasChanged(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Pages/HelloWorld.razor: -------------------------------------------------------------------------------- 1 | @page "/helloworld" 2 | 3 | 4 | @if (state != null) 5 | { 6 |

@state.Greetings

7 |
8 |

Server time is: @state.ServerTime

9 |

Enter your name:

10 |
11 | 12 | 13 | 14 |
15 | 16 |
17 | } 18 |
19 | 20 | @code { 21 | private IHelloWorldState state; 22 | private Person person = new Person(); 23 | 24 | private void UpdateState(IHelloWorldState state) 25 | { 26 | this.state = state; 27 | StateHasChanged(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Hello, world!

4 | 5 | Welcome to your new app. 6 | 7 | 8 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using DotNetify.Blazor; 5 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Basic.Client 9 | { 10 | public class Program 11 | { 12 | public static async Task Main(string[] args) 13 | { 14 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 15 | builder.RootComponents.Add("app"); 16 | 17 | builder.Services.AddDotNetifyBlazor(config => config.Debug = true); 18 | builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 19 | 20 | await builder.Build().RunAsync(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Templates/Basic/Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 6 | 7 |
8 |
9 | About 10 |
11 | 12 |
13 | @Body 14 |
15 |
16 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 31 |
32 | 33 | @code { 34 | private bool collapseNavMenu = true; 35 | 36 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 37 | 38 | private void ToggleNavMenu() 39 | { 40 | collapseNavMenu = !collapseNavMenu; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Templates/Basic/Client/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 |  11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Templates/Basic/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 7 | @using Microsoft.JSInterop 8 | @using Basic.Client 9 | @using Basic.Client.Shared 10 | @using Basic.Shared 11 | @using DotNetify.Blazor; 12 | -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Templates/Basic/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Templates/Basic/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Templates/Basic/Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Basic 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 |
17 | An unhandled error has occurred. 18 | Reload 19 | 🗙 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Templates/Basic/Server/Basic.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Templates/Basic/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Basic.Server 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Templates/Basic/Server/Services/WeatherForecastService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Basic.Shared; 5 | 6 | namespace Basic.Server 7 | { 8 | public class WeatherForecastService 9 | { 10 | private static readonly string[] Summaries = new[] 11 | { 12 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 13 | }; 14 | 15 | public Task GetAsync() 16 | { 17 | var rng = new Random(); 18 | var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 19 | { 20 | Date = DateTime.Now.AddDays(index), 21 | TemperatureC = rng.Next(-20, 55), 22 | Summary = Summaries[rng.Next(Summaries.Length)] 23 | }) 24 | .ToArray(); 25 | 26 | return Task.FromResult(weatherForecasts); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Templates/Basic/Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace Basic.Server 9 | { 10 | public class Startup 11 | { 12 | public Startup(IConfiguration configuration) 13 | { 14 | Configuration = configuration; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddSignalR(); 22 | services.AddDotNetify(); 23 | 24 | services.AddTransient(); 25 | } 26 | 27 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 28 | { 29 | if (env.IsDevelopment()) 30 | { 31 | app.UseDeveloperExceptionPage(); 32 | app.UseWebAssemblyDebugging(); 33 | } 34 | else 35 | { 36 | app.UseExceptionHandler("/Error"); 37 | } 38 | 39 | app.UseWebSockets(); 40 | app.UseDotNetify(); 41 | 42 | app.UseBlazorFrameworkFiles(); 43 | app.UseStaticFiles(); 44 | app.UseRouting(); 45 | 46 | app.UseEndpoints(endpoints => 47 | { 48 | endpoints.MapHub("/dotnetify"); 49 | endpoints.MapFallbackToFile("index.html"); 50 | }); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Templates/Basic/Server/ViewModels/Counter.cs: -------------------------------------------------------------------------------- 1 | using Basic.Shared; 2 | using DotNetify; 3 | 4 | namespace Basic.Server 5 | { 6 | public class Counter : BaseVM, ICounterState 7 | { 8 | public int CurrentCount 9 | { 10 | get => Get(); 11 | set => Set(value); 12 | } 13 | 14 | public void IncrementCount() => CurrentCount++; 15 | } 16 | } -------------------------------------------------------------------------------- /Templates/Basic/Server/ViewModels/FetchData.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Basic.Shared; 3 | using DotNetify; 4 | 5 | namespace Basic.Server 6 | { 7 | public class FetchData : BaseVM, IFetchDataState 8 | { 9 | private readonly WeatherForecastService weatherForecastService; 10 | 11 | public WeatherForecast[] Forecasts { get; set; } 12 | 13 | public FetchData(WeatherForecastService weatherForecastService) 14 | { 15 | this.weatherForecastService = weatherForecastService; 16 | } 17 | 18 | public override async Task OnCreatedAsync() 19 | { 20 | Forecasts = await weatherForecastService.GetAsync(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Templates/Basic/Server/ViewModels/HelloWorld.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Basic.Shared; 4 | using DotNetify; 5 | 6 | namespace Basic.Server 7 | { 8 | public class HelloWorld : BaseVM, IHelloWorldState 9 | { 10 | private readonly Timer timer; 11 | 12 | public string Greetings 13 | { 14 | get => Get(); 15 | set => Set(value); 16 | } 17 | 18 | public DateTime ServerTime 19 | { 20 | get => Get(); 21 | set => Set(value); 22 | } 23 | 24 | public HelloWorld() 25 | { 26 | Greetings = "Hello World"; 27 | ServerTime = DateTime.Now; 28 | 29 | timer = new Timer(state => 30 | { 31 | ServerTime = DateTime.Now; 32 | PushUpdates(); 33 | }, null, 0, 1000); 34 | } 35 | 36 | public override void Dispose() 37 | { 38 | timer.Dispose(); 39 | } 40 | 41 | public void Submit(Person person) 42 | { 43 | Greetings = $"Hello {person.FirstName} {person.LastName}!"; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Templates/Basic/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Templates/Basic/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Templates/Basic/Shared/Basic.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Templates/Basic/Shared/ICounterState.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.Shared 2 | { 3 | public interface ICounterState 4 | { 5 | int CurrentCount { get; set; } 6 | 7 | void IncrementCount(); 8 | } 9 | } -------------------------------------------------------------------------------- /Templates/Basic/Shared/IFetchDataState.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.Shared 2 | { 3 | public interface IFetchDataState 4 | { 5 | WeatherForecast[] Forecasts { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Templates/Basic/Shared/IHelloWorldState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Basic.Shared 6 | { 7 | public interface IHelloWorldState 8 | { 9 | string Greetings { get; set; } 10 | DateTime ServerTime { get; set; } 11 | 12 | void Submit(Person person); 13 | } 14 | } -------------------------------------------------------------------------------- /Templates/Basic/Shared/Person.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.Shared 2 | { 3 | public class Person 4 | { 5 | public string FirstName { get; set; } 6 | public string LastName { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Templates/Basic/Shared/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Basic.Shared 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public string Summary { get; set; } 12 | 13 | public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); 14 | } 15 | } -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | DotNetify.Blazor.UnitTests 9 | 10 | DotNetify.Blazor.UnitTests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Website/Website.Client/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |

Sorry, there's nothing at this address.

7 |
8 |
9 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/AWSIntegration.razor: -------------------------------------------------------------------------------- 1 | @page "/aws-integration" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Basics.razor: -------------------------------------------------------------------------------- 1 | @page "/basics" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/DataFlow.razor: -------------------------------------------------------------------------------- 1 | @page "/dataflow" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/GetStarted.razor: -------------------------------------------------------------------------------- 1 | @page "/getstarted" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Reactive.razor: -------------------------------------------------------------------------------- 1 | @page "/reactive" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/RealtimePostgres.razor: -------------------------------------------------------------------------------- 1 | @page "/postgres" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Scaleout.razor: -------------------------------------------------------------------------------- 1 | @page "/scaleout" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Snippets/Details.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | @code { 6 | private IDetailsState state; 7 | 8 | public interface IDetailsState 9 | { 10 | string ItemImageUrl { get; set; } 11 | } 12 | 13 | private void UpdateState(IDetailsState state) 14 | { 15 | this.state = state; 16 | StateHasChanged(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Snippets/Master.razor: -------------------------------------------------------------------------------- 1 |  2 | @if(state != null) 3 | { 4 | foreach(var item in state?.ListItems) 5 | { 6 |
  • @item.Title
  • 7 | } 8 | } 9 |
    10 | 11 | @code { 12 | private IMasterState state; 13 | private string selectedId; 14 | 15 | public interface IMasterState 16 | { 17 | ListItem[] ListItems { get; set; } 18 | void Select(string id); 19 | } 20 | 21 | public class ListItem 22 | { 23 | public string Id { get; set; } 24 | public string Title { get; set; } 25 | } 26 | 27 | private string GetItemStyle(string id) 28 | { 29 | var color = id == this.selectedId ? "#eee" : "none"; 30 | return $"cursor:pointer; background:{color}"; 31 | } 32 | 33 | private void HandleSelect(string id) 34 | { 35 | this.selectedId = id; 36 | this.state.Select(id); 37 | } 38 | 39 | private void UpdateState(IMasterState state) 40 | { 41 | this.state = state; 42 | HandleSelect(state.ListItems[0].Id); 43 | StateHasChanged(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Snippets/MasterDetails.razor: -------------------------------------------------------------------------------- 1 |  2 |
    3 |
    4 |
    5 |
    6 |
    -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Snippets/RealtimePush.razor: -------------------------------------------------------------------------------- 1 |  2 |
    3 |

    @state?.Greetings

    4 |

    Server time is: @state?.ServerTime

    5 |
    6 |
    7 | 8 | @code { 9 | private IHelloWorldState state; 10 | 11 | public interface IHelloWorldState 12 | { 13 | string Greetings { get; set; } 14 | string ServerTime { get; set; } 15 | } 16 | 17 | private void UpdateState(IHelloWorldState state) 18 | { 19 | this.state = state; 20 | StateHasChanged(); 21 | } 22 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/1_Overview/Snippets/ServerUpdate.razor: -------------------------------------------------------------------------------- 1 |  2 |
    3 |

    @state?.Greetings

    4 | 5 | 6 | 7 |
    8 |
    9 | 10 | @code { 11 | private IServerUpdateState state; 12 | private Person person = new Person(); 13 | 14 | public interface IServerUpdateState 15 | { 16 | string Greetings { get; set; } 17 | void Submit(Person person); 18 | } 19 | 20 | public class Person 21 | { 22 | public string FirstName { get; set; } 23 | public string LastName { get; set; } 24 | } 25 | 26 | private void UpdateState(IServerUpdateState state) 27 | { 28 | this.state = state; 29 | StateHasChanged(); 30 | } 31 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Chatroom.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | max-width: 1268px; 3 | } 4 | 5 | footer { 6 | max-width: 1268px; 7 | padding: 0.5rem; 8 | font-size: small; 9 | font-style: italic; 10 | margin-top: 1rem; 11 | display: flex; 12 | flex-direction: row; 13 | } 14 | 15 | .chatPanel { 16 | height: calc(100vh - 23rem); 17 | max-width: 1268px; 18 | display: flex; 19 | flex: 1; 20 | flex-direction: row; 21 | border: 1px solid #ccc; 22 | border-radius: 0.2rem; 23 | 24 | nav { 25 | padding: 1rem; 26 | min-width: 11rem; 27 | border-right: 1px solid #ccc; 28 | overflow: auto; 29 | 30 | b { 31 | font-weight: 500; 32 | 33 | &.myself { 34 | text-decoration: underline; 35 | } 36 | } 37 | 38 | p > *:not(b) { 39 | font-size: x-small; 40 | line-height: 0.8rem; 41 | color: #999; 42 | } 43 | 44 | p > span { 45 | display: block; 46 | } 47 | } 48 | 49 | section { 50 | display: flex; 51 | flex-direction: column; 52 | flex: 1; 53 | background: #fff; 54 | 55 | .private { 56 | font-style: italic; 57 | } 58 | 59 | > div:first-child { 60 | padding: 1rem; 61 | flex: 1; 62 | overflow: auto; 63 | 64 | > div { 65 | margin-bottom: 0.5rem; 66 | 67 | span:first-child { 68 | font-weight: 500; 69 | margin-right: 1rem; 70 | } 71 | 72 | span:last-child { 73 | font-size: x-small; 74 | color: #999; 75 | } 76 | } 77 | } 78 | 79 | > div:last-child { 80 | input { 81 | border: none; 82 | border-top: 1px solid #ccc; 83 | border-top-left-radius: unset; 84 | border-top-right-radius: unset; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/CompositeView/CompositeView.razor: -------------------------------------------------------------------------------- 1 | @page "/compositeview" 2 | @inject IStyleSheet StyleSheet 3 | 4 | 5 | 6 | 7 |
    8 | 9 | 10 | 11 |
    12 | 16 |
    17 |
    18 |
    -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/CompositeView/CompositeView.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | padding: 0 1rem; 3 | max-width: 1268px; 4 | display: flex; 5 | flex-wrap: wrap; 6 | 7 | > section { 8 | width: 70%; 9 | margin-right: 1rem; 10 | } 11 | 12 | > aside { 13 | width: calc(30% - 2rem); 14 | min-width: 10rem; 15 | } 16 | 17 | @media (max-width: 960px) { 18 | > section { 19 | width: 100%; 20 | margin-bottom: 2rem; 21 | } 22 | 23 | > aside { 24 | width: 100%; 25 | } 26 | } 27 | 28 | .card { 29 | width: 100%; 30 | 31 | p { 32 | font-size: unset; 33 | } 34 | 35 | .card-header > *:first-child { 36 | font-size: large; 37 | } 38 | } 39 | 40 | table { 41 | font-size: unset; 42 | width: 100%; 43 | 44 | tr { 45 | &:hover { 46 | background: #eee; 47 | cursor: pointer; 48 | } 49 | 50 | &.selected { 51 | background: #ddd; 52 | } 53 | } 54 | 55 | td, 56 | th { 57 | width: 25%; 58 | padding: 0.5rem; 59 | padding-right: 2rem; 60 | border-bottom: 1px solid #ddd; 61 | 62 | &:nth-of-type(1), 63 | &:nth-of-type(3) { 64 | width: 10%; 65 | } 66 | } 67 | } 68 | 69 | .pagination { 70 | user-select: none; 71 | 72 | div { 73 | margin-top: 1rem; 74 | padding: 0.5rem 1rem; 75 | border: 1px solid #ccc; 76 | 77 | &.current { 78 | color: #aaa; 79 | background: #eee; 80 | } 81 | } 82 | 83 | div:hover:not(.current) { 84 | background: #eee; 85 | cursor: pointer; 86 | } 87 | } 88 | 89 | .filter { 90 | margin-top: 1rem; 91 | 92 | .card-header { 93 | font-weight: bold; 94 | } 95 | 96 | .card-body { 97 | > * { 98 | margin-bottom: 1rem; 99 | } 100 | } 101 | 102 | .card-footer { 103 | display: flex; 104 | justify-content: flex-end; 105 | } 106 | 107 | .chip { 108 | margin-right: 0.25rem; 109 | margin-bottom: 0.25rem; 110 | display: inline-flex; 111 | font-size: x-small; 112 | background: #eee; 113 | border-radius: 0.25rem; 114 | align-items: center; 115 | padding: 0.2rem 0.4rem; 116 | 117 | i.material-icons { 118 | font-size: small; 119 | font-weight: bold; 120 | cursor: pointer; 121 | margin-left: 0.5rem; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/CompositeView/MovieDetails.razor: -------------------------------------------------------------------------------- 1 |  2 | @if (state != null) 3 | { 4 |
    5 |
    6 | @state.Movie.Movie 7 |
    @state.Movie.Year
    8 |
    9 |
    10 | Director 11 |

    @state.Movie.Director

    12 | Cast 13 |
    14 | @foreach (var cast in state.Movie.Cast.Split(",")) 15 | { 16 |
    @cast
    17 | } 18 |
    19 |
    20 |
    21 | } 22 |
    23 | 24 | @code { 25 | private IMovieDetailsState state; 26 | 27 | public interface IMovieDetailsState 28 | { 29 | MovieTable.MovieRecord Movie { get; set; } 30 | } 31 | 32 | private void UpdateState(IMovieDetailsState state) 33 | { 34 | this.state = state; 35 | StateHasChanged(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/CompositeView/MovieTable.razor: -------------------------------------------------------------------------------- 1 |  2 | @if (state != null) 3 | { 4 |
    5 | 6 | 7 | 8 | @foreach (var header in state.Headers) 9 | { 10 | 11 | } 12 | 13 | 14 | 15 | @foreach (var data in state.Data) 16 | { 17 | 20 | 21 | 22 | 23 | 24 | 25 | } 26 | 27 |
    @header
    @data.Rank@data.Movie@data.Year@data.Director
    28 | 38 |
    39 | } 40 |
    41 | 42 | @code { 43 | private IMovieTableState state; 44 | 45 | public interface IMovieTableState 46 | { 47 | string[] Headers { get; set; } 48 | MovieRecord[] Data { get; set; } 49 | int[] Pagination { get; set; } 50 | [Watch] int SelectedKey { get; set; } 51 | [Watch] int SelectedPage { get; set; } 52 | } 53 | 54 | public class MovieRecord 55 | { 56 | public int Rank { get; set; } 57 | public string Movie { get; set; } 58 | public int Year { get; set; } 59 | public string Director { get; set; } 60 | public string Cast { get; set; } 61 | } 62 | 63 | private void UpdateState(IMovieTableState state) 64 | { 65 | this.state = state; 66 | StateHasChanged(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/ControlTypes.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | max-width: 1268px; 3 | padding: 0 1rem; 4 | b { 5 | color: #ce4844; 6 | font-weight: 500; 7 | } 8 | 9 | table { 10 | font-size: unset; 11 | width: 100%; 12 | } 13 | 14 | tr { 15 | td { 16 | padding-bottom: 1.5rem; 17 | } 18 | 19 | td:first-child { 20 | font-weight: 500; 21 | display: flex; 22 | flex-direction: column; 23 | > label { 24 | font-weight: normal; 25 | font-size: small; 26 | } 27 | } 28 | 29 | td:nth-of-type(2) { 30 | width: 70%; 31 | padding-right: 1.5rem; 32 | li { 33 | cursor: pointer; 34 | } 35 | 36 | li:hover { 37 | background: #efefef; 38 | } 39 | 40 | input ~ * { 41 | margin-left: 0rem; 42 | margin-right: 0rem; 43 | > li { 44 | padding-left: 0.75rem; 45 | } 46 | } 47 | 48 | label { 49 | cursor: pointer; 50 | user-select: none; 51 | margin-right: 1.5rem; 52 | } 53 | } 54 | 55 | button.label-success { 56 | margin-left: 3rem; 57 | background: #5cb85c; 58 | } 59 | 60 | button.label-warning { 61 | margin-left: 3rem; 62 | background: #f0ad4e; 63 | } 64 | 65 | button { 66 | min-width: 6rem; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Dashboard/ActivitiesCard.razor: -------------------------------------------------------------------------------- 1 | @inject IStyleSheet StyleSheet 2 | 3 | 4 | 5 |

    Activities

    6 | @foreach (var activity in Activities) 7 | { 8 | 9 |
    10 | @getInitial(activity.PersonName) 11 | @activity.PersonName 12 |
    13 | 14 |
    15 | @activity.Status 16 | 17 |
    18 |
    19 |
    20 | } 21 |
    22 |
    23 | 24 | @code { 25 | [Parameter] public Dashboard.Activity[] Activities { get; set; } 26 | 27 | private static readonly string[] statusColors = new string[] { "", "silver", "limegreen", "red", "gray", "orange" }; 28 | private static readonly string[] userIconColors = new string[] { "#00ce6f", "#a95df0", "#2ea7eb" }; 29 | 30 | private char getInitial(string name) => name.ToUpper()[0]; 31 | 32 | private string getCss(Dashboard.Activity activity) 33 | { 34 | string iconColor = userIconColors[getInitial(activity.PersonName) % 3]; 35 | string statusColor = statusColors[activity.StatusId]; 36 | 37 | return StyleSheet["ActivitiesCard"] 38 | .Replace("$icon-color", iconColor) 39 | .Replace("$status-color", statusColor); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Dashboard/ActivitiesCard.razor.scss: -------------------------------------------------------------------------------- 1 | .user-icon { 2 | display: inline-flex; 3 | flex-direction: column; 4 | width: 25px; 5 | height: 25px; 6 | border-radius: 50%; 7 | color: white; 8 | 9 | background: $icon-color; 10 | font-weight: bold; 11 | margin-right: 1rem; 12 | text-align: center; 13 | } 14 | 15 | .status-icon { 16 | height: 14px; 17 | width: 14px; 18 | margin-left: 1rem; 19 | 20 | background-color: $status-color; 21 | border-radius: 50%; 22 | display: inline-block; 23 | } 24 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Dashboard/Dashboard.razor: -------------------------------------------------------------------------------- 1 | @page "/dashboard" 2 | 3 | 4 | 5 | @if (state == null) 6 | { 7 |
    8 | } 9 | else 10 | { 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

    Network Traffic

    31 | 32 |
    33 | 34 |

    Utilization

    35 | 36 |
    37 |
    38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 |

    Server Usage

    46 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 | } 53 | 54 | 55 | 56 | @code { 57 | private IDashboardState state; 58 | 59 | public class Activity 60 | { 61 | public int Id { get; set; } 62 | public string PersonName { get; set; } 63 | public string Status { get; set; } 64 | public int StatusId { get; set; } 65 | } 66 | 67 | public interface IDashboardState 68 | { 69 | Activity[] RecentActivities { get; set; } 70 | } 71 | 72 | private void UpdateState(IDashboardState state) 73 | { 74 | this.state = state; 75 | StateHasChanged(); 76 | } 77 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Dashboard/InfoCard.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 |

    9 |
    10 |
    11 |
    12 | 13 | @code { 14 | [Parameter] public string Id { get; set; } 15 | [Parameter] public string Color { get; set; } 16 | 17 | public string ReplaceTokenInCss(string css) => css.Replace("$icon-bg-color", Color); 18 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Dashboard/InfoCard.razor.scss: -------------------------------------------------------------------------------- 1 | d-card { 2 | display: flex; 3 | .card-body { 4 | padding: 0.5rem 1.5rem; 5 | min-width: 12rem; 6 | 7 | @media (max-width: 1600px) { 8 | min-width: 24rem; 9 | } 10 | @media (max-width: 1270px) { 11 | min-width: 16rem; 12 | } 13 | } 14 | > label, 15 | > h3 { 16 | display: none; 17 | } 18 | } 19 | 20 | h3 { 21 | font: 600 2rem Helvetica; 22 | } 23 | 24 | i { 25 | font-size: 3rem; 26 | max-width: 6rem; 27 | padding: 1.5rem; 28 | opacity: 0.8; 29 | color: white; 30 | background: $icon-bg-color; 31 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/AddressForm.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/Form.razor: -------------------------------------------------------------------------------- 1 | @page "/form" 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @if (showDialog) 23 | { 24 | 25 | } 26 | 27 | 28 | 29 | 30 | @code { 31 | private bool editMode; 32 | private bool showDialog; 33 | 34 | private string IsEditing => editMode.ToString().ToLower(); 35 | private string IsViewing => (!editMode).ToString().ToLower(); 36 | 37 | private void HandleElementEvent(ElementEventArgs elementEvent) 38 | { 39 | if (elementEvent.TargetId == "_Edit" || elementEvent.TargetId == "_Cancel" || elementEvent.TargetId == "Submit") 40 | editMode = !editMode; 41 | else if (elementEvent.TargetId == "_New") 42 | ToggleDialog(); 43 | 44 | StateHasChanged(); 45 | } 46 | 47 | private void ToggleDialog() 48 | { 49 | showDialog = !showDialog; 50 | StateHasChanged(); 51 | } 52 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/FormTabs.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/NewFormDialog.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 | 18 | 19 | 20 | 21 |
    22 |
    23 |
    24 | 25 | @code { 26 | [Parameter] public EventCallback OnClosed { get; set; } 27 | 28 | private string isOpen = "true"; 29 | 30 | private void HandleElementEvent(ElementEventArgs e) 31 | { 32 | if (e.TargetId == "_Cancel" || e.TargetId == "Submit") 33 | { 34 | isOpen = "false"; 35 | StateHasChanged(); 36 | _ = Task.Delay(250 /* wait for closing animation*/).ContinueWith(_ => OnClosed.InvokeAsync(EventCallback.Empty)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/PersonForm.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/Form/PhoneForm.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/HelloWorld.razor: -------------------------------------------------------------------------------- 1 | @page "/helloworld" 2 | 3 | 4 | 5 | @if (state != null) 6 | { 7 | 8 |
    9 |
    10 | 11 | 12 |
    13 |
    14 | 15 | 16 |
    17 |
    18 |
    19 | Full name is @state.FullName 20 |
    21 |
    22 | } 23 |
    24 |
    25 | 26 | @code { 27 | private IHelloWorldState state; 28 | 29 | public interface IHelloWorldState 30 | { 31 | [Watch] string FirstName { get; set; } 32 | [Watch] string LastName { get; set; } 33 | string FullName { get; set; } 34 | } 35 | 36 | private void UpdateState(IHelloWorldState state) 37 | { 38 | this.state = state; 39 | StateHasChanged(); 40 | } 41 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/HelloWorld.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | max-width: 1268px; 3 | } 4 | 5 | section { 6 | display: flex; 7 | margin-bottom: 1rem; 8 | > div { 9 | flex: 1; 10 | &:last-child { 11 | margin-left: 1rem; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/SecurePage/AdminSecurePage.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Http 2 | 3 | 4 | @if (state != null) 5 | { 6 |
    7 |
    Admin-only view:
    8 |
    @state.TokenIssuer
    9 |
    @state.TokenValidFrom
    10 |
    @state.TokenValidTo
    11 |
    12 | 13 |
    14 | } 15 |
    16 | 17 | @code { 18 | [Parameter] public string AccessToken { get; set; } 19 | 20 | private IAdminSecurePageState state; 21 | private VMConnectOptions connectOptions; 22 | 23 | public interface IAdminSecurePageState 24 | { 25 | string TokenIssuer { get; set; } 26 | string TokenValidFrom { get; set; } 27 | string TokenValidTo { get; set; } 28 | 29 | void Dispatch(Dictionary properties); 30 | } 31 | 32 | protected override void OnInitialized() 33 | { 34 | this.connectOptions = new VMConnectOptions 35 | { 36 | Headers = new Dictionary 37 | { 38 | { "Authorization", "Bearer " + AccessToken } 39 | } 40 | }; 41 | } 42 | 43 | private async Task HandleRefreshAsync(MouseEventArgs e) 44 | { 45 | var response = await Login.FetchTokenAsync(Http, "admin", "dotnetify"); 46 | if (response.IsSuccessStatusCode) 47 | { 48 | var result = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync()); 49 | this.state.Dispatch(new Dictionary 50 | { 51 | { "$headers", new { Authorization = "Bearer " + result.access_token } }, 52 | { "Refresh", true } 53 | }); 54 | } 55 | } 56 | 57 | private void UpdateState(IAdminSecurePageState state) 58 | { 59 | this.state = state; 60 | StateHasChanged(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/SecurePage/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/securepage" 2 | @inject HttpClient Http 3 | 4 | 5 | 6 |
    7 | @if (accessToken != null) 8 | { 9 |
    10 | 11 |
    12 | 13 | } 14 | else 15 | { 16 |
    17 |
    18 |

    Sign in

    19 |
    20 |
    21 |
    22 | 23 | 27 | @loginError 28 |
    29 |
    30 | 31 | 32 | @loginError 33 |
    34 |
    35 | 38 |
    39 |
    40 |
    41 |

    Not authenticated

    42 |
    43 |
    44 | } 45 |
    46 |
    47 |
    48 | 49 | @code { 50 | private string userName; 51 | private string password = "dotnetify"; 52 | private string accessToken; 53 | private string loginError; 54 | 55 | internal class TokenResponse 56 | { 57 | public string access_token { get; set; } 58 | } 59 | 60 | private async Task Submit() 61 | { 62 | var response = await FetchTokenAsync(Http, this.userName, this.password); 63 | if (response.IsSuccessStatusCode) 64 | { 65 | var result = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync()); 66 | this.accessToken = result.access_token; 67 | this.loginError = string.Empty; 68 | } 69 | else 70 | this.loginError = "Invalid user name or password"; 71 | StateHasChanged(); 72 | } 73 | 74 | private void SignOut() 75 | { 76 | accessToken = null; 77 | StateHasChanged(); 78 | } 79 | 80 | internal static Task FetchTokenAsync(HttpClient http, string userName, string password) 81 | { 82 | var content = new Dictionary(); 83 | content.Add("username", userName); 84 | content.Add("password", password); 85 | content.Add("grant_type", "password"); 86 | content.Add("client_id", "dotnetifydemo"); 87 | 88 | return http.PostAsync("/token", new FormUrlEncodedContent(content)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/SecurePage/Login.razor.scss: -------------------------------------------------------------------------------- 1 | > div { 2 | padding: 0 1rem; 3 | max-width: 1268px; 4 | display: flex; 5 | > * { 6 | flex: 1; 7 | margin-right: 1rem; 8 | } 9 | b { 10 | color: #ce4844; 11 | font-weight: 500; 12 | } 13 | label { 14 | font-weight: 500; 15 | margin-top: 1rem; 16 | } 17 | .card-body > div:last-child { 18 | margin-bottom: 2rem; 19 | } 20 | .card-body > strong { 21 | display: block; 22 | margin-bottom: 1rem; 23 | } 24 | .card-footer { 25 | display: flex; 26 | justify-content: flex-end; 27 | } 28 | .logout { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | } 33 | } -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/SecurePage/SecurePage.razor: -------------------------------------------------------------------------------- 1 |  2 | @if (state != null) 3 | { 4 |
    5 |
    6 |

    @state.SecureCaption

    7 |
    8 |
    9 |

    10 | @state.SecureData 11 |

    12 | 13 |
    14 |
    15 | } 16 |
    17 | 18 | @code { 19 | [Parameter] public string AccessToken { get; set; } 20 | [Parameter] public EventCallback OnExpiredAccess { get; set; } 21 | 22 | private ISecurePageState state; 23 | private VMConnectOptions connectOptions; 24 | 25 | public interface ISecurePageState 26 | { 27 | string SecureCaption { get; set; } 28 | string SecureData { get; set; } 29 | } 30 | 31 | protected override void OnInitialized() 32 | { 33 | this.connectOptions = new VMConnectOptions 34 | { 35 | Headers = new Dictionary 36 | { 37 | { "Authorization", "Bearer " + AccessToken } 38 | } 39 | }; 40 | } 41 | 42 | private void HandleException(ExceptionEventArgs e) 43 | { 44 | if (e.Name == nameof(UnauthorizedAccessException)) 45 | OnExpiredAccess.InvokeAsync(null); 46 | } 47 | 48 | private void UpdateState(ISecurePageState state) 49 | { 50 | this.state = state; 51 | StateHasChanged(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/2_Examples/SimpleList.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | padding: 0 1rem; 3 | max-width: 1268px; 4 | } 5 | 6 | .alert { 7 | display: flex; 8 | align-items: center; 9 | .material-icons { 10 | font-size: 24px; 11 | width: 2rem; 12 | } 13 | } 14 | 15 | header { 16 | display: flex; 17 | align-items: center; 18 | margin-bottom: 1rem; 19 | > * { 20 | margin-right: 1rem; 21 | } 22 | input[type='text'] { 23 | max-width: 15rem; 24 | } 25 | } 26 | 27 | table { 28 | font-size: unset; 29 | width: 100%; 30 | max-width: 1268px; 31 | td, 32 | th { 33 | padding: 0.5rem 0; 34 | padding-right: 2rem; 35 | border-bottom: 1px solid #ddd; 36 | width: 50%; 37 | } 38 | th { 39 | font-weight: 500; 40 | } 41 | td:last-child, 42 | th:last-child { 43 | width: 5rem; 44 | > div { 45 | display: flex; 46 | align-items: center; 47 | cursor: pointer; 48 | } 49 | } 50 | tr:hover { 51 | background: #efefef; 52 | span.editable { 53 | &:after { 54 | font-family: 'Material Icons'; 55 | content: 'edit'; 56 | color: #ccc; 57 | } 58 | } 59 | span.editable:hover { 60 | &:after { 61 | color: black; 62 | } 63 | } 64 | } 65 | i.material-icons { 66 | font-size: 1.2rem; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/CRUD.razor: -------------------------------------------------------------------------------- 1 | @page "/crud" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/DI.razor: -------------------------------------------------------------------------------- 1 | @page "/di" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/DotNetClient.razor: -------------------------------------------------------------------------------- 1 | @page "/dotnetclient" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/Filter.razor: -------------------------------------------------------------------------------- 1 | @page "/filter" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/Middleware.razor: -------------------------------------------------------------------------------- 1 | @page "/middleware" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/Multicast.razor: -------------------------------------------------------------------------------- 1 | @page "/multicast" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/ScopedCss.razor: -------------------------------------------------------------------------------- 1 | @page "/scopedcss" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/Security.razor: -------------------------------------------------------------------------------- 1 | @page "/security" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/3_APIReferences/WebApiMode.razor: -------------------------------------------------------------------------------- 1 | @page "/webapimode" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/4_Premium/DotNetifyLoadTester.razor: -------------------------------------------------------------------------------- 1 | @page "/dotnetify-loadtester" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/4_Premium/DotNetifyObserver.razor: -------------------------------------------------------------------------------- 1 | @page "/dotnetify-observer" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/4_Premium/DotNetifyResiliencyAddon.razor: -------------------------------------------------------------------------------- 1 | @page "/dotnetify-resiliencyaddon" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/4_Premium/DotNetifyTesting.razor: -------------------------------------------------------------------------------- 1 | @page "/dotnetify-testing" 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Website/Website.Client/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | -------------------------------------------------------------------------------- /Website/Website.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using DotNetify.Blazor; 5 | using Microsoft.AspNetCore.Components.Web; 6 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace Website.Client 10 | { 11 | public class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 16 | 17 | builder.Services 18 | .AddDotNetifyBlazor(config => 19 | { 20 | #if DEBUG 21 | config.Debug = true; 22 | #endif 23 | //config.WebSocketServerUrl = "ws://localhost:3000"; 24 | }) 25 | .AddScoped(_ => new HttpClient 26 | { 27 | BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) 28 | }); 29 | 30 | builder.RootComponents.Add("app"); 31 | builder.RootComponents.Add("head::after"); 32 | await builder.Build().RunAsync(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/ArticleLayout.razor: -------------------------------------------------------------------------------- 1 | @inject IStyleSheet StyleSheet 2 | 3 | 4 | @if(state != null) 5 | { 6 | 7 | 8 | @ChildContent 9 | 10 | 11 | @if(!string.IsNullOrEmpty(Id)) 12 | { 13 | 14 | 15 | 16 | } 17 | 18 | 19 | } 20 | 21 | 22 | @code { 23 | [Parameter] public string VM { get; set; } 24 | [Parameter] public string Id { get; set; } = "Content"; 25 | [Parameter] public RenderFragment ChildContent { get; set; } 26 | 27 | private object state; 28 | 29 | private void UpdateState(object state) 30 | { 31 | this.state = state; 32 | StateHasChanged(); 33 | } 34 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/ArticleLayout.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | margin-top: 1.5rem; 3 | margin-left: 10%; 4 | margin-right: 0; 5 | max-width: 1268px; 6 | @media (max-width: 1170px) { 7 | margin-left: 2rem; 8 | max-width: calc(100% - 2rem); 9 | } 10 | @media (max-width: 414px) { 11 | margin-left: 1rem; 12 | max-width: calc(100% - 1rem); 13 | } 14 | 15 | .article { 16 | max-width: calc(100% - 30rem); 17 | min-width: 65%; 18 | @media (max-width: 1170px) { 19 | max-width: calc(100% - 2rem); 20 | } 21 | @media (max-width: 414px) { 22 | max-width: calc(100% - 1rem); 23 | } 24 | } 25 | .sidebar { 26 | position: fixed; 27 | border-left: 1px solid orange; 28 | margin-left: 2rem; 29 | padding-left: 1rem; 30 | p { 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/ExampleLayout.razor: -------------------------------------------------------------------------------- 1 | @inject IStyleSheet StyleSheet 2 | 3 | 4 | @if (state != null) 5 | { 6 | 7 | 8 | 9 | @ChildContent 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | } 42 | 43 | 44 | @code { 45 | [Parameter] public string VM { get; set; } 46 | [Parameter] public RenderFragment ChildContent { get; set; } 47 | 48 | private object state; 49 | 50 | private void UpdateState(object state) 51 | { 52 | this.state = state; 53 | StateHasChanged(); 54 | } 55 | 56 | private void HandleElementEvent(ElementEventArgs e) 57 | { 58 | System.Diagnostics.Trace.WriteLine(string.Format("{0} {1} {2}", e.EventName, e.EventArgs, e.TargetId)); 59 | } 60 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/Expander.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | @ChildContent 4 | 5 | 6 | 7 | @code { 8 | [Parameter] public RenderFragment ChildContent { get; set; } 9 | [Parameter] public string Label { get; set; } 10 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/Expander.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | margin: 1rem 0; 3 | padding: 0.5rem; 4 | border-radius: 5px; 5 | border: 1px solid #ccc; 6 | background: #ddd; 7 | [class^='Label__LabelContainer'] { 8 | font-weight: bold; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/GithubLink.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | 12 | 13 | GitHub 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | Sponsor 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/GithubLink.razor.scss: -------------------------------------------------------------------------------- 1 | .github-sponsor { 2 | border: 1px solid #ccc; 3 | border-radius: 6px; 4 | padding: 3px 12px; 5 | background: #fafbfc; 6 | text-align: center; 7 | max-width: 8rem; 8 | box-shadow: 0 1px 0 rgba(27, 31, 35, 0.04), inset 0 1px 0 hsla(0, 0%, 100%, 0.25); 9 | a { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | svg { 15 | color: #ea4aaa; 16 | fill: currentColor; 17 | margin-right: 5px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @inject IStyleSheet StyleSheet 3 | @inject NavigationManager NavigationManager 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 | 19 |
    20 | 21 | 22 | 23 | 24 |
    25 | 26 | 27 | @Body 28 | 29 | 30 | 31 |
    32 | © 2015-2020 Dicky Suryadi. Licensed under the  33 | Apache license version 2.0 34 |
    35 |
    36 |
    37 | 38 | @code { 39 | private readonly string dotNetifyIcon = Website.Client.Properties.Resources.DotNetifyIcon; 40 | private readonly string basePath = "/"; 41 | private string selectedRoute; 42 | 43 | protected override void OnInitialized() 44 | { 45 | this.selectedRoute = ToRouteId(NavigationManager.Uri); 46 | NavigationManager.LocationChanged += (sender, e) => { selectedRoute = ToRouteId(e.Location); StateHasChanged(); }; 47 | } 48 | 49 | private string ToRouteId(string uri) => this.basePath + NavigationManager.ToBaseRelativePath(uri); 50 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/MainLayout.razor.scss: -------------------------------------------------------------------------------- 1 | .license-notice { 2 | margin-top: auto; 3 | margin-left: auto; 4 | padding: 5px 8px; 5 | font-size: 0.8rem; 6 | color: #999; 7 | a { 8 | color: #337ab7; 9 | &:hover { 10 | color: #0056b3; 11 | text-decoration: none; 12 | } 13 | &:focus { 14 | color: #337ab7; 15 | > * { 16 | background: #e7e7e7; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Website/Website.Client/Shared/MenuLinks.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Core 5 | 6 | 7 | Elements 8 | 9 | 10 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/MenuLinks.razor.scss: -------------------------------------------------------------------------------- 1 | & { 2 | margin-left: 11%; 3 | 4 | @media (max-width: 1170px) { 5 | margin-left: 5.5rem; 6 | } 7 | @media (max-width: 767px) { 8 | display: none; 9 | } 10 | } 11 | 12 | a { 13 | padding: .2rem .5rem; 14 | font-size: medium; 15 | font-weight: 500; 16 | color: #aaa; 17 | 18 | &.active { 19 | color: #444; 20 | &:hover { 21 | background: #f3f3f3; 22 | color: #666; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/SelectFramework.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 10 | 11 | 12 | @code { 13 | private void HandleChange(ChangeEventArgs args) 14 | { 15 | string framework = args.Value.ToString(); 16 | if (framework != "Blazor") 17 | NavigationManager.NavigateTo("https://dotnetify.net/core?" + framework.ToLower()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/SelectFramework.razor.scss: -------------------------------------------------------------------------------- 1 | select { 2 | width: calc(100% - 1.5rem); 3 | margin-left: 0.75rem; 4 | margin-top: 1rem; 5 | font-weight: 500; 6 | border-color: #92d050; 7 | background-color: transparent; 8 | &:focus { 9 | background-color: transparent; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/TwitterLink.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Website/Website.Client/Shared/TwitterLink.razor.scss: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | 4 | @media (max-width: 320px) { 5 | display: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Website/Website.Client/Website.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | True 38 | Resources.resx 39 | 40 | 41 | 42 | 43 | 44 | ResXFileCodeGenerator 45 | Resources.Designer.cs 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Website/Website.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Text.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.JSInterop 7 | @using Website.Client 8 | @using Website.Client.Shared 9 | @using DotNetify.Blazor 10 | -------------------------------------------------------------------------------- /Website/Website.Client/wwwroot/app.js: -------------------------------------------------------------------------------- 1 | app = { 2 | setFocus: function (id) { 3 | document.getElementById(id).focus(); 4 | } 5 | } -------------------------------------------------------------------------------- /Website/Website.Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-font-smoothing: antialiased; 3 | font-size: .9rem; 4 | } 5 | 6 | body { 7 | background-color: #f4f4f4; 8 | height: 100vh; 9 | } 10 | 11 | d-header { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | d-header > * { 17 | height: unset !important; 18 | } 19 | 20 | strong { 21 | font-weight: 500; 22 | color: #6aa828; 23 | background-color: rgba(255, 255, 255, .6); 24 | padding: 0 4px; 25 | border-radius: 2px; 26 | } 27 | 28 | .theme-dark strong { 29 | background-color: rgba(255, 255, 255, .2); 30 | } 31 | 32 | p, 33 | .markdown ol > li, 34 | .markdown div > ul > li { 35 | font-size: 1.1rem; 36 | line-height: 1.8rem; 37 | } 38 | 39 | div > h2 { 40 | margin-bottom: 1rem; 41 | } 42 | 43 | .markdown b { 44 | font-weight: 500; 45 | background-color: rgba(255, 255, 255, .6); 46 | } 47 | 48 | .markdown pre { 49 | margin: 0; 50 | font-size: .9rem; 51 | } 52 | 53 | .markdown pre .language-jsx, 54 | .markdown pre .language-html { 55 | font-size: 1rem; 56 | } 57 | 58 | .markdown pre span.prism-code { 59 | margin-top: 1rem; 60 | margin-bottom: 1rem; 61 | } 62 | 63 | .markdown pre + p { 64 | margin-top: 1rem; 65 | } 66 | 67 | .markdown h2 { 68 | margin-bottom: 2rem; 69 | } 70 | 71 | .markdown h4 { 72 | margin-bottom: 1.5rem; 73 | padding-bottom: .5rem; 74 | border-bottom: 1px solid #ccc; 75 | } 76 | 77 | .markdown * + * > h4, 78 | .markdown * + h4 { 79 | margin-top: 3rem; 80 | } 81 | 82 | .markdown blockquote { 83 | border-left: .5rem solid #ccc; 84 | padding-left: 1rem; 85 | margin-left: 3rem; 86 | font-style: italic; 87 | } 88 | 89 | .toc-h5 { 90 | font-size: .9rem; 91 | } 92 | 93 | @media (max-width: 768px) { 94 | html, 95 | .markdown pre, 96 | .markdown pre .language-jsx { 97 | font-size: .9rem; 98 | } 99 | } 100 | 101 | .theme-dark td { 102 | color: #fff; 103 | } 104 | 105 | .theme-dark tr:hover td { 106 | color: #333; 107 | } 108 | 109 | .theme-dark .card { 110 | background-color: #333; 111 | } 112 | 113 | .lds-hourglass { 114 | margin: 1rem; 115 | display: inline-block; 116 | position: relative; 117 | width: 80px; 118 | height: 80px; 119 | } 120 | 121 | .lds-hourglass:after { 122 | content: " "; 123 | display: block; 124 | border-radius: 50%; 125 | width: 0; 126 | height: 0; 127 | margin: 8px; 128 | box-sizing: border-box; 129 | border: 32px solid #fff; 130 | border-color: #fff transparent #fff transparent; 131 | animation: lds-hourglass 1.2s infinite; 132 | } 133 | 134 | @keyframes lds-hourglass { 135 | 0% { 136 | transform: rotate(0); 137 | animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); 138 | } 139 | 140 | 50% { 141 | transform: rotate(900deg); 142 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 143 | } 144 | 145 | 100% { 146 | transform: rotate(1800deg); 147 | } 148 | } -------------------------------------------------------------------------------- /Website/Website.Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsuryd/dotNetify-Blazor/5bf6d96ad77a3ab49067bd9155721445cfaa1a9e/Website/Website.Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Website/Website.Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | dotNetify-Blazor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Website/Website.Server/AuthServer.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using AspNet.Security.OpenIdConnect.Extensions; 6 | using AspNet.Security.OpenIdConnect.Primitives; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace Website.Server 12 | { 13 | public static class AuthServer 14 | { 15 | public const string SecretKey = "my_secretkey_123!"; 16 | 17 | // Source: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server 18 | public static void AddAuthenticationServer(this IServiceCollection services) 19 | { 20 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)); 21 | 22 | services.AddAuthentication().AddOpenIdConnectServer(options => 23 | { 24 | options.AccessTokenHandler = new JwtSecurityTokenHandler(); 25 | options.SigningCredentials.AddKey(signingKey); 26 | 27 | options.AllowInsecureHttp = true; 28 | options.TokenEndpointPath = "/token"; 29 | 30 | options.Provider.OnValidateTokenRequest = context => 31 | { 32 | context.Validate(); 33 | return Task.CompletedTask; 34 | }; 35 | 36 | options.Provider.OnHandleTokenRequest = context => 37 | { 38 | if (context.Request.Password != "dotnetify") 39 | { 40 | context.Reject( 41 | error: OpenIdConnectConstants.Errors.InvalidGrant, 42 | description: "Invalid user credentials."); 43 | return Task.CompletedTask; 44 | } 45 | 46 | var identity = new ClaimsIdentity(context.Scheme.Name, 47 | OpenIdConnectConstants.Claims.Name, 48 | OpenIdConnectConstants.Claims.Role); 49 | 50 | identity.AddClaim(OpenIdConnectConstants.Claims.Name, context.Request.Username); 51 | identity.AddClaim(OpenIdConnectConstants.Claims.Subject, context.Request.Username); 52 | 53 | identity.AddClaim(ClaimTypes.Name, context.Request.Username, 54 | OpenIdConnectConstants.Destinations.AccessToken, 55 | OpenIdConnectConstants.Destinations.IdentityToken); 56 | 57 | if (context.Request.Username == "admin") 58 | { 59 | identity.AddClaim(ClaimTypes.Role, "admin", 60 | OpenIdConnectConstants.Destinations.AccessToken, 61 | OpenIdConnectConstants.Destinations.IdentityToken); 62 | } 63 | 64 | var ticket = new AuthenticationTicket( 65 | new ClaimsPrincipal(identity), 66 | new AuthenticationProperties(), 67 | context.Scheme.Name); 68 | 69 | ticket.SetAccessTokenLifetime(System.TimeSpan.FromSeconds(30)); 70 | 71 | ticket.SetScopes( 72 | OpenIdConnectConstants.Scopes.Profile, 73 | OpenIdConnectConstants.Scopes.OfflineAccess); 74 | 75 | context.Validate(ticket); 76 | return Task.CompletedTask; 77 | }; 78 | }); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /Website/Website.Server/Docs/CRUD.md: -------------------------------------------------------------------------------- 1 | ## CRUD 2 | 3 | CRUD are such a common task that the APIs to help you with it is baked in the **BaseVM**. Let's say your app has a view that displays a list of items, therefore your view model would have a property that returns that list: 4 | 5 | ```csharp 6 | public class ContactList : BaseVM 7 | { 8 | private readonly IContactService _contactService; 9 | public class Contact 10 | { 11 | public int Id { get; set; } 12 | public string Name { get; set; } 13 | public string Email { get; set; } 14 | } 15 | public List Contacts => _contactService.GetAllContacts(); 16 | 17 | public ContactList( IContactService contactService ) 18 | { 19 | _contactService = contactService; 20 | } 21 | } 22 | ``` 23 | 24 | When you need to add, update or remove an item on the list from the back-end, you can use the following APIs: 25 | 26 | - this.**AddList**(_propertyName, item_) 27 | - this.**UpdateList**(_propertyName, item_) 28 | - this.**RemoveList**(_propertyName, itemKey_) 29 | 30 | But using any of this API will require you decorate the list property with an **[ItemKey]** attribute to specify the item key of the list. For example: 31 | 32 | ```csharp 33 | [ItemKey(nameof(Contact.Id))] 34 | public List Contacts => _contactService.GetAllContacts(); 35 | 36 | public void Add(Contact contact) => this.AddList(nameof(Contacts), contact); 37 | public void Update(Contact contact) => this.UpdateList(nameof(Contacts), contact); 38 | public void Remove(Contact contact) => this.RemoveList(nameof(Contacts), contact.Id); 39 | ``` 40 | 41 | Note that any of these APIs will merely mark the list property as changed. The update will be sent only if it's part of the action-reaction cycle, or if you initiate the push yourself by calling **PushUpdates**. Once it gets to the front-end, the change will be made to the associated React component's state, which will then cause the list to be re-rendered. 42 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/DI.md: -------------------------------------------------------------------------------- 1 | ## Dependency Injection 2 | 3 | DotNetify is integrated with the built-in ASP.NET DI container. Service registration is typically done in the _ConfigureServices_ method of the application's _Startup_ class. For example: 4 | 5 | ```csharp 6 | public void ConfigureServices(IServiceCollection services) 7 | { 8 | services.AddSignalR(); 9 | services.AddDotNetify(); 10 | 11 | services.AddTransient(); 12 | services.AddSingleton(); 13 | } 14 | ``` 15 | 16 | When instantiating view models, dotNetify uses the _ActivatorUtilities_ class from _Microsoft.Extensions.DependencyInjection_, which will automatically resolve any dependency to registered services. This class is also capable of injecting concrete types without the need for explicit registration. 17 | 18 | ```csharp 19 | public class MyViewModel : BaseVM 20 | { 21 | private readonly IServiceA _serviceA; 22 | private readonly IServiceB _serviceB; 23 | 24 | public MyViewModel(IServiceA serviceA, IServiceB serviceB) 25 | { 26 | _serviceA = serviceA; 27 | _serviceB = serviceB; 28 | } 29 | } 30 | ``` 31 | 32 | Services with scoped lifetime are created once per persistent connection. 33 | 34 | #### Factory Method 35 | 36 | If you need a greater control over dotNetify's object creation, you have the option to set up a custom factory method: 37 | 38 | ```csharp 39 | app.UseDotNetify(config => { 40 | config.SetFactoryMethod((type, args) => 41 | { 42 | /* return an instance of the specified type */ 43 | }); 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/DotNetClient.md: -------------------------------------------------------------------------------- 1 | ## .NET Client 2 | 3 | DotNetify can be used to let your .NET apps, such as a WPF desktop app, communicate with view models hosted on an ASP.NET Core server. To do this, add the __DotNetify.SignalR__ package to your project, then use the __DotNetify.Client__ namespace to get to the __DotNetifyClient__ class. 4 | 5 | #### Dependency Injection 6 | 7 | You can manually instantiate __DotNetifyClient__ and its dependencies, but it is recommended to use the DI container from the _Microsoft.Extensions.DependencyInjection_ assembly: 8 | 9 | ```csharp 10 | using System; 11 | using DotNetify; 12 | using DotNetify.Client; 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | namespace MyApp 16 | { 17 | public static class ServiceProvider 18 | { 19 | private static IServiceProvider _serviceProvider; 20 | 21 | static ServiceProvider() 22 | { 23 | _serviceProvider = new ServiceCollection() 24 | .AddDotNetifyClient() 25 | .BuildServiceProvider(); 26 | } 27 | 28 | public static T Resolve() => _serviceProvider.GetRequiredService(); 29 | } 30 | } 31 | ``` 32 | 33 | Then get a dotNetify client instance by: 34 | ```csharp 35 | IDotNetifyClient dotnetify = ServiceProvider.Resolve(); 36 | ``` 37 | 38 | #### Usage 39 | 40 | Start by creating a class that will serve as a proxy to the server-side view model. It needs to implement _INotifyPropertyChanged_ and _IDisposable_ interfaces, and provide public properties with the same names and types to its counterpart: 41 | 42 | ```csharp 43 | public class HelloWorldProxy : INotifyPropertyChanged, IDisposable 44 | { 45 | private readonly IDotNetifyClient _dotnetify; 46 | 47 | public string Greetings { get; set; } 48 | public string ServerTime { get; set; } 49 | 50 | public HelloWorldProxy(IDotNetifyClient dotnetify) 51 | { 52 | _dotnetify = dotnetify; 53 | _dotnetify.ConnectAsync("HelloWorld", this); 54 | } 55 | 56 | public void Dispose() => _dotnetify.Dispose(); 57 | } 58 | ``` 59 | 60 | The APIs are similar to the Javascript client's: 61 | - __ConnectAsync__(_string vmName, INotifyPropertyChanged proxy, VMConnectOptions options_)
    62 | 63 | The options object provide the following properties: 64 | - __VMArg__: object to initialize view model properties. 65 | For example: 66 | ```csharp 67 | _dotnetify.ConnectAsync("HelloWorld", this, 68 | new VMConnectOptions { VMArg = new { Greetings = "Hello!" } }); 69 | ``` 70 | - __Headers__: object, pass request headers, e.g. for authentication purpose. 71 | 72 | 73 | - __DispatchAsync__(_string propertyName, object propertyValue_) 74 | - __DispatchAsync__(_Dictionary propertyValues_) 75 | - __DisposeAsync__() 76 | 77 | #### UI Dispatcher 78 | 79 | If you use this with a UI framework, update to _ObservableCollection_ objects may require the operation to be conducted in the UI thread. You need to provide your own implementation of the __IUIDispatcher__ interface and include it in the service registration. For example: 80 | 81 | ```csharp 82 | // WPF 83 | public class WpfUIThreadDispatcher : IUIThreadDispatcher 84 | { 85 | public Task InvokeAsync(Action action) => Application.Current.Dispatcher.InvokeAsync(action); 86 | } 87 | 88 | // Avalonia 89 | public class AvaloniaUIThreadDispatcher : IUIThreadDispatcher 90 | { 91 | public Task InvokeAsync(Action action) => Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(action); 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/Examples/HelloWorld.md: -------------------------------------------------------------------------------- 1 |  2 | ##### HelloWorld.razor 3 | 4 | ```jsx 5 | 6 | @if (state != null) 7 | { 8 | 9 |
    10 |
    11 | 12 | 13 |
    14 |
    15 | 16 | 17 |
    18 |
    19 |
    20 | Full name is @state.FullName 21 |
    22 |
    23 | } 24 |
    25 | 26 | @code { 27 | private IHelloWorldState state; 28 | 29 | public interface IHelloWorldState 30 | { 31 | [Watch] string FirstName { get; set; } 32 | [Watch] string LastName { get; set; } 33 | string FullName { get; set; } 34 | } 35 | 36 | private void UpdateState(IHelloWorldState state) 37 | { 38 | this.state = state; 39 | StateHasChanged(); 40 | } 41 | } 42 | ``` 43 | 44 |
    45 | 46 | ##### HelloWorld.cs 47 | 48 | ```csharp 49 | public class HelloWorldVM : BaseVM 50 | { 51 | private string _firstName = "Hello"; 52 | private string _lastName = "World"; 53 | 54 | public string FirstName 55 | { 56 | get => _firstName; 57 | set 58 | { 59 | _firstName = value; 60 | Changed(nameof(FullName)); 61 | } 62 | } 63 | 64 | public string LastName 65 | { 66 | get => _lastName; 67 | set 68 | { 69 | _lastName = value; 70 | Changed(nameof(FullName)); 71 | } 72 | } 73 | 74 | public string FullName => $"{FirstName} {LastName}"; 75 | } 76 | ``` 77 | 78 | 79 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/Filter.md: -------------------------------------------------------------------------------- 1 | ## View Model Filter 2 | 3 | View model filters are similar to middlewares in which they too have access to raw client requests before they are resolved by the view models. However, they are only executed at the back-end of the request pipeline, when the view model instance associated with the request has been identified, so they can be given the opportunity to interact directly with the view model. 4 | 5 | A view model filter is always paired with an attribute class. The attribute serves to identify which view model type that the filter targets, and can also be provided with arguments for the filter. 6 | 7 | To implement a view model filter, create both an attribute and a class that inherits from __IVMFilter__ where _T_ is the attribute type. For example: 8 | 9 | ```csharp 10 | [AttributeUsage(AttributeTargets.Class)] 11 | public class AuthorizeAttribute : Attribute {} 12 | 13 | public class AuthorizeFilter : IVMFilter 14 | { 15 | public Task Invoke(AuthorizeAttribute auth, VMContext context, NextFilterDelegate next) 16 | { 17 | if (context.HubContext.Principal?.Identity?.IsAuthenticated == false) 18 | throw new UnauthorizedAccessException(); 19 | 20 | return next(context); 21 | } 22 | } 23 | ``` 24 | 25 | Use the filter on a view model by adorning the class with the attribute. When the client request is received, the filter can either pass the control to the next filter in the pipeline until it reaches the view model, halt the execution by not invoking the next delegate, or throw an exception. 26 | 27 | #### Filter Registration 28 | 29 | To register the filter to the pipeline, add it to dotNetify's configuration with __UseFilter__. 30 | 31 | ```csharp 32 | public void Configuration(IAppBuilder app) 33 | { 34 | ... 35 | app.UseDotNetify(config => { 36 | config.UseFilter(); 37 | }); 38 | } 39 | ``` -------------------------------------------------------------------------------- /Website/Website.Server/Docs/Overview/GetStarted.md: -------------------------------------------------------------------------------- 1 | ## Get Started 2 | 3 | The quickest way to start is to download [the basic template](https://github.com/dsuryd/dotNetify-Blazor/tree/master/Templates). Use the steps below to add to an existing project. 4 | 5 | #### Server Setup 6 | 7 | Download the NuGet package **DotNetify.SignalR**, then add the following in the _Startup.cs_: 8 | 9 | ```csharp 10 | public void ConfigureServices(IServiceCollection services) 11 | { 12 | services.AddSignalR(); 13 | services.AddDotNetify(); 14 | ... 15 | } 16 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 17 | { 18 | app.UseWebSockets(); 19 | app.UseDotNetify(); 20 | 21 | app.UseRouting(); 22 | app.UseEndpoints(endpoints => endpoints.MapHub("/dotnetify")); 23 | ... 24 | } 25 | ``` 26 | 27 |
    28 | 29 | The default configuration assumes all view model classes are in the web project, and uses built-in .NET dependency injection. To override the configuration: 30 | 31 | ```csharp 32 | app.UseDotNetify(config => { 33 | config.RegisterAssembly(/* name of the assembly where the view model classes are located */); 34 | config.SetFactoryMethod((type, args) => /* let your favorite IoC library creates the view model instance */); 35 | }); 36 | ``` 37 | 38 | #### Client Setup 39 | 40 | Download the NuGet package **DotNetify.Blazor**, then add the following to the `index.html`: 41 | 42 | ```html 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | Add the **AddDotNetifyBlazor** service extension to the _Main_ method in _Program.cs_: 55 | 56 | ```csharp 57 | public static async Task Main(string[] args) 58 | { 59 | ... 60 | builder.Services.AddDotNetifyBlazor(); 61 | ... 62 | } 63 | ``` 64 | 65 | Add the **DotNetify.Blazor** namespace in _\_Imports.razor_: 66 | 67 | ```jsx 68 | ... 69 | @using DotNetify.Blazor; 70 | ``` 71 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/Overview/Overview.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | DotNetify makes it super easy to connect your [Blazor WebAssembly](https://docs.microsoft.com/en-us/aspnet/core/blazor/) client app to the server in a declarative, real-time and reactive manner. It uses [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/) to communicate with the server through an MVVM-styled abstraction, but unlike Blazor Server, you get to choose what is sent over the network and when. 4 | 5 | DotNetify is a good fit for building complex web applications that requires clear separation of concerns between client-side UI, presentation, and domain logic for long-term maintainability and extensibility. Plus, its integration with SignalR allows you to easily implement asynchronous data updates to multiple clients in real-time. 6 | 7 | #### Features 8 | 9 | DotNetify's design is based on the principle of maintaining strong separation of concerns that places business or data-driven logic firmly on the back-end, putting the front-end only in charge of UI/UX concerns. Some of the things dotNetify support: 10 | 11 | - Server-side view model: don't let your client download too much code; keep most processing on the back-end and only send things that change. 12 | 13 | - Declarative state hydration: eliminate the need to write data-fetching boilerplate services. Send data to the back-end by simply invoking an interface method. 14 | 15 | - Simple real-time abstraction: push data to client in real-time from multiple classes with no coupling to low-level SignalR details. 16 | 17 | - **[Portable to AWS](aws-integration)**: switch from self-hosted SignalR to using the Amazon WebSocket API Gateway with HTTP back-end integration with a few simple steps. 18 | 19 | - **[Scoped CSS](scopedcss)**: built-in support for SASS-like syntax and replacing styles at runtime! 20 | 21 | - **[Web Components](https://dotnetify.net/elements?webcomponent)**: comes with _Elements_, a library of HTML native web components which makes it very convenient to implement layouts, online forms, charts, and more --- all supporting CSS isolation. Usage is optional! 22 | 23 | - **[Reusable with Javascript SPAs](https://github.com/dsuryd/dotNetify/tree/master/Demo)**: Can't always use Blazor? The same view models you write for Blazor can be reused with Javascript UI frameworks without change. DotNetify has full support for React and Vue, and can be made to work with Angular and others. 24 | 25 | - **[Reusable with .NET desktop clients](https://github.com/dsuryd/dotNetify/tree/master/Demo/DotNetClient)**: reuse the same view models with .NET-based client apps using WPF or Avalonia. 26 | 27 | - **[Multicasting](multicast)**: send real-time data to multiple clients at once; perfect for real-time collaboration / data synchronization. 28 | 29 | - **[Reactive]()**: make your view model declarative with streaming, observable properties. Supports asynchronous programming. 30 | 31 | - **[Dependency injection](di)**: inject dependency objects through the class constructor. 32 | 33 | - **[Middlewares /](middleware) [filters](filter)**: build a pipeline to do all sorts of things before reaching the view models. 34 | 35 | - **[Bearer token authentication](security)**: pass authentication header as payload instead of query string. 36 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/Premium/DotNetifyResiliencyAddon.md: -------------------------------------------------------------------------------- 1 | ## DotNetify-ResiliencyAddon 2 | 3 | _DotNetify-ResiliencyAddon_ allows your dotNetify app server to be more resilient when serving as an HTTP integration backend to the Amazon WebSocket API gateway. When configured with a distributed cache such as Redis, existing connections will be able to survive a server restart. Any active view model instance will be recreated and can resume its activities with connected clients when the server is restored. 4 | 5 | 6 | 7 | This is a closed-source library for Pro, Team, and Enterprise sponsors. If you are one, send an email to _admin@dotnetify.net_ with your username to get your license key. 8 | 9 | - Pro sponsor: single developer, up to 100 client connections. 10 | - Team sponsor: up to 10 developers, unlimited usage + private email support. 11 | - Enterprise sponsor: unlimited usage + private email support. 12 | 13 | 14 | 15 | #### Installation 16 | 17 | Add **DotNetify.ResiliencyAddon** from NuGet to your project. 18 | 19 | #### Setup 20 | 21 | Add the following in the _Startup.cs_: 22 | 23 | ```csharp 24 | using DotNetify.WebApi; 25 | 26 | ... 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | ... 30 | /* Place this after services.AddDotNetifyIntegrationWebApi() */ 31 | services.AddDotNetifyResiliencyAddon(); 32 | services.AddStackExchangeRedisCache(options => options.Configuration = ""); 33 | ... 34 | } 35 | 36 | public void Configure(IApplicationBuilder app) 37 | { 38 | ... 39 | app.UseDotNetify(); 40 | app.UseDotNetifyResiliencyAddon(); 41 | ... 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/ScopedCss.md: -------------------------------------------------------------------------------- 1 | ## Scoped CSS 2 | 3 | While Blazor has native support for component-scoped CSS, dotNetify offers its own powerful implementation via the **StyleSheet** component. It works by looking for an embedded resource by the same name of the component where it is declared (or can be explicitly provided), loads the content as string and provides it to a custom web component which will apply the style sheet only to the DOM elements nested within it. 4 | 5 | #### Basic Usage 6 | 7 | To create a style sheet for a component, first add a new file to contain the style sheet and name it the same as your component's name with a `css` or `scss` extension. For example: _MyComponent.razor.scss_. Make sure to set the build action of the file to _Embedded Resource_. Then in your component, declare this: 8 | 9 | ```jsx 10 | ... 11 | ``` 12 | 13 | The **Context** attribute set to `this` will tell the component to find the resource matching the type name. If you want to provide a different name, use the **Name** attribute instead. 14 | 15 | #### Style Sheet Syntax 16 | 17 | The component uses a light CSS preprocessor to build style sheet, which allows SASS-like nesting capabilities such as this: 18 | 19 | ```jsx 20 | & { 21 | .button { 22 | &:hover { 23 | background-color: #eee; 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | #### Parameterized Style Sheet 30 | 31 | If you ever have the need to replace certain styles in the style sheet at runtime, the component provides the **OnLoad** event to allow the opportunity to modify the string content. You could insert a token in the style sheet, and replace it with an actual style on the component's load event: 32 | 33 | ```jsx 34 | 35 | ... 36 | 37 | 38 | @code { 39 | private string ChangeColor(string css) 40 | { 41 | return css.Replace("$color-to-replace", "red"); 42 | } 43 | } 44 | ``` 45 | 46 | #### Usage with Web Components 47 | 48 | Most of _Element_'s web components accept a style sheet string through their `css` attribute. If you want to get a style sheet from an embedded resource and provide the string straight to this attribute, use the **IStyleSheet** service: 49 | 50 | ```jsx 51 | @inject IStyleSheet StyleSheet 52 | 53 | 54 | ... 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /Website/Website.Server/Docs/WebApiMode.md: -------------------------------------------------------------------------------- 1 | ## Web API Mode 2 | 3 | When real-time communication isn't a requirement and you don't want to use SignalR, but you still want to write your back-end code using the MVVM pattern, you can specify your UI component to use the web API mode when connecting. 4 | 5 | The web API mode will cause your component to send requests to the DotNetify Web API endpoint instead of the usual SignalR hub. To enable mode, you will need to add the MVC services to the _Startup.cs_, and on the client-side, set the **WebApi** parameter of the _VMContext_ component: 6 | 7 | ```csharp 8 | using DotNetify.WebApi; 9 | 10 | public void ConfigureServices(IServiceCollection services) 11 | { 12 | services.AddDotNetify(); 13 | services.AddMvc(); 14 | ... 15 | } 16 | public void Configure(IApplicationBuilder app) 17 | { 18 | app.UseDotNetify(); 19 | app.UseEndpoints(endpoints => endpoints.MapControllers()); 20 | ... 21 | } 22 | ``` 23 | 24 | ```jsx 25 | 26 | ``` 27 | 28 | Keep in mind that other than the fact that real-time functionality like _PushUpdates_ and _multicasting_ won't be working in this mode, every API request will create a new instance of the view model and dispose it on completion. Therefore, the correct way to implement a view model for this mode is to make it stateless. 29 | 30 | The headers that you've configured in the _connect_ options will be set to the HTTP request headers. The middlewares and filters will continue to work, with the _DotNetifyHubContext_ argument using the information from _HttpContext_. 31 | -------------------------------------------------------------------------------- /Website/Website.Server/ExamplePipelines/ExtractAccessTokenMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotNetify.Security; 3 | using Microsoft.IdentityModel.Tokens; 4 | using System.Threading.Tasks; 5 | using DotNetify; 6 | 7 | namespace Website.Server 8 | { 9 | public class ExtractAccessTokenMiddleware : JwtBearerAuthenticationMiddleware 10 | { 11 | public ExtractAccessTokenMiddleware(TokenValidationParameters tokenValidationParameters) : base(tokenValidationParameters) 12 | { 13 | } 14 | 15 | public override Task Invoke(DotNetifyHubContext hubContext, NextDelegate next) 16 | { 17 | if (hubContext.Headers != null) 18 | { 19 | try 20 | { 21 | ValidateBearerToken(ParseHeaders(hubContext.Headers), out SecurityToken validatedToken); 22 | if (validatedToken != null) 23 | hubContext.PipelineData.Add("AccessToken", validatedToken); 24 | } 25 | catch (Exception) 26 | { 27 | } 28 | } 29 | 30 | return next(hubContext); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Website/Website.Server/ExamplePipelines/SetAccessTokenFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Threading.Tasks; 3 | using DotNetify; 4 | using Microsoft.IdentityModel.Tokens; 5 | 6 | namespace Website.Server 7 | { 8 | /// 9 | /// Custom filter to set JWT access token to any view model that has AccessToken property. 10 | /// 11 | public class SetAccessTokenFilter : IVMFilter 12 | { 13 | public Task Invoke(SetAccessTokenAttribute attr, VMContext vmContext, NextFilterDelegate next) 14 | { 15 | var methodInfo = vmContext.Instance.GetType().GetTypeInfo().GetMethod("SetAccessToken"); 16 | var accessToken = vmContext.HubContext.PipelineData.ContainsKey("AccessToken") ? vmContext.HubContext.PipelineData["AccessToken"] : null; 17 | 18 | if (methodInfo != null && accessToken != null) 19 | methodInfo.Invoke(vmContext.Instance, new object[] { accessToken as SecurityToken }); 20 | 21 | return next(vmContext); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Website/Website.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace Website.Server 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } -------------------------------------------------------------------------------- /Website/Website.Server/Properties/PublishProfiles/FTPProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | FTP 9 | True 10 | Release 11 | Any CPU 12 | / 13 | False 14 | 0ee42035-815f-4a38-84b9-76c10fb35245 15 | 206.217.202.60 16 | True 17 | True 18 | / 19 | blazor 20 | <_SavePWD>True 21 | net6.0 22 | false 23 | 24 | -------------------------------------------------------------------------------- /Website/Website.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:8090/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 18 | }, 19 | "Website.Server": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:8090/", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:8090/" 27 | }, 28 | "Website.Server.Debug": { 29 | "commandName": "Project", 30 | "launchBrowser": true, 31 | "launchUrl": "http://localhost:8090/", 32 | "environmentVariables": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "applicationUrl": "http://localhost:8090/", 36 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Website/Website.Server/Services/EmployeeRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bogus; 5 | 6 | namespace Website.Server 7 | { 8 | public class Employee 9 | { 10 | public int Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public string ReportTo { get; set; } 14 | public string Phone { get; set; } 15 | } 16 | 17 | public interface IEmployeeRepository 18 | { 19 | Task> GetAllAsync(int count); 20 | 21 | IEnumerable GetAll(int count); 22 | 23 | Task GetAsync(int id); 24 | 25 | Employee Get(int id); 26 | 27 | Task AddAsync(Employee model); 28 | 29 | int Add(Employee model); 30 | 31 | Task UpdateAsync(Employee model); 32 | 33 | void Update(Employee model); 34 | 35 | Task RemoveAsync(int id); 36 | 37 | void Remove(int id); 38 | } 39 | 40 | public class EmployeeRepository : IEmployeeRepository 41 | { 42 | private IList _mockData; 43 | 44 | public async Task> GetAllAsync(int count) 45 | { 46 | await Task.Delay(10); 47 | return GetAll(count); 48 | } 49 | 50 | public IEnumerable GetAll(int count) 51 | { 52 | return _mockData ?? GenerateMockData(count); 53 | } 54 | 55 | public async Task GetAsync(int id) 56 | { 57 | await Task.Delay(10); 58 | return Get(id); 59 | } 60 | 61 | public Employee Get(int id) 62 | { 63 | return _mockData.FirstOrDefault(x => x.Id == id); 64 | } 65 | 66 | public async Task AddAsync(Employee model) 67 | { 68 | await Task.Delay(10); 69 | return Add(model); 70 | } 71 | 72 | public int Add(Employee model) 73 | { 74 | var employee = new Employee 75 | { 76 | Id = _mockData.Count > 0 ? _mockData.Max(x => x.Id) + 1 : 1, 77 | FirstName = model.FirstName, 78 | LastName = model.LastName, 79 | ReportTo = model.ReportTo, 80 | Phone = model.Phone 81 | }; 82 | 83 | _mockData.Add(employee); 84 | return employee.Id; 85 | } 86 | 87 | public async Task UpdateAsync(Employee model) 88 | { 89 | await Task.Delay(10); 90 | Update(model); 91 | } 92 | 93 | public void Update(Employee model) 94 | { 95 | var employee = Get(model.Id); 96 | 97 | employee.FirstName = model.FirstName; 98 | employee.LastName = model.LastName; 99 | employee.ReportTo = model.ReportTo; 100 | employee.Phone = model.Phone; 101 | } 102 | 103 | public async Task RemoveAsync(int id) 104 | { 105 | await Task.Delay(10); 106 | Remove(id); 107 | } 108 | 109 | public void Remove(int id) 110 | { 111 | var employee = Get(id); 112 | _mockData.Remove(employee); 113 | } 114 | 115 | private IList GenerateMockData(int count) 116 | { 117 | int id = 0; 118 | var list = new Faker() 119 | .CustomInstantiator(f => new Employee { Id = ++id }) 120 | .RuleFor(o => o.FirstName, f => f.Person.FirstName) 121 | .RuleFor(o => o.LastName, f => f.Person.LastName) 122 | .RuleFor(o => o.ReportTo, f => f.Person.FullName) 123 | .RuleFor(o => o.Phone, f => f.Phone.PhoneNumber("(###) ###-####")) 124 | .Generate(count); 125 | 126 | _mockData = list; 127 | return list; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /Website/Website.Server/Services/MovieService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using DotNetify.Elements; 4 | using Newtonsoft.Json; 5 | 6 | namespace Website.Server 7 | { 8 | public interface IMovieService 9 | { 10 | IEnumerable GetAFITop100(); 11 | 12 | MovieRecord GetMovieByAFIRank(int rank); 13 | } 14 | 15 | public class MovieRecord 16 | { 17 | public int Rank { get; set; } 18 | public string Movie { get; set; } 19 | public int Year { get; set; } 20 | public string Cast { get; set; } 21 | 22 | public string MovieCast => Cast; 23 | public string Director { get; set; } 24 | } 25 | 26 | public class MovieService : IMovieService 27 | { 28 | public IEnumerable GetAFITop100() => JsonConvert.DeserializeObject>( 29 | Utils.GetResource("Website.Server.Services.AFITop100.json", GetType().Assembly).Result); 30 | 31 | public MovieRecord GetMovieByAFIRank(int rank) => GetAFITop100().FirstOrDefault(i => i.Rank == rank); 32 | } 33 | } -------------------------------------------------------------------------------- /Website/Website.Server/Services/WebStoreService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | using DotNetify.Elements; 5 | using System.Threading.Tasks; 6 | 7 | namespace Website.Server 8 | { 9 | public interface IWebStoreService 10 | { 11 | Task> GetAllBooksAsync(); 12 | 13 | Task GetBookByTitleAsync(string title); 14 | 15 | Task GetBookByIdAsync(int id); 16 | } 17 | 18 | public class WebStoreRecord 19 | { 20 | public int Id { get; set; } 21 | public string Type { get; set; } 22 | public string Category { get; set; } 23 | public bool Recommended { get; set; } 24 | public string Title { get; set; } 25 | public string Author { get; set; } 26 | public float Rating { get; set; } 27 | public string ImageUrl { get; set; } 28 | public string ItemUrl { get; set; } 29 | public string UrlSafeTitle => ToUrlSafe(Title); 30 | 31 | public static string ToUrlSafe(string title) => title.ToLower() 32 | .Replace("\'", "") 33 | .Replace(".", "dot") 34 | .Replace("#", "sharp") 35 | .Replace(' ', '-'); 36 | } 37 | 38 | public class WebStoreService : IWebStoreService 39 | { 40 | public async Task> GetAllRecordsAsync() => JsonConvert.DeserializeObject>( 41 | await Utils.GetResource("Website.Server.Services.webstore.json", GetType().Assembly)); 42 | 43 | public async Task> GetAllBooksAsync() => (await GetAllRecordsAsync()).Where(i => i.Type == "Book"); 44 | 45 | public async Task GetBookByTitleAsync(string title) => (await GetAllBooksAsync()).FirstOrDefault(i => i.UrlSafeTitle == title); 46 | 47 | public async Task GetBookByIdAsync(int id) => (await GetAllBooksAsync()).FirstOrDefault(i => i.Id == id); 48 | } 49 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/CompositeView/CompositeView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using DotNetify; 4 | using DotNetify.Elements; 5 | 6 | namespace Website.Server 7 | { 8 | public class CompositeViewVM : BaseVM 9 | { 10 | private readonly IMovieService _movieService; 11 | 12 | private event EventHandler Selected; 13 | 14 | public CompositeViewVM(IMovieService movieService) 15 | { 16 | _movieService = movieService; 17 | } 18 | 19 | public override void OnSubVMCreated(BaseVM subVM) 20 | { 21 | if (subVM is FilterableMovieTableVM) 22 | InitMovieTableVM(subVM as FilterableMovieTableVM); 23 | else if (subVM is MovieDetailsVM) 24 | InitMovieDetailsVM(subVM as MovieDetailsVM); 25 | } 26 | 27 | private void InitMovieTableVM(FilterableMovieTableVM vm) 28 | { 29 | // Set the movie table data source to AFI Top 100 movies. 30 | vm.DataSource = () => _movieService.GetAFITop100(); 31 | 32 | // When movie table selection changes, raise a private Selected event. 33 | vm.Selected += (sender, rank) => Selected?.Invoke(this, rank); 34 | } 35 | 36 | private void InitMovieDetailsVM(MovieDetailsVM vm) 37 | { 38 | // Set default details to the highest ranked movie. 39 | vm.SetByAFIRank(1); 40 | 41 | // When the Selected event occurs, update the movie details. 42 | Selected += (sender, rank) => vm.SetByAFIRank(rank); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/CompositeView/FilterableMovieTableVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Dynamic.Core; 5 | using DotNetify; 6 | 7 | namespace Website.Server 8 | { 9 | public class FilterableMovieTableVM : BaseVM 10 | { 11 | private Func> _dataSourceFunc; 12 | private Action _updateData; 13 | private string _query; 14 | 15 | public event EventHandler Selected; 16 | 17 | public Func> DataSource 18 | { 19 | set 20 | { 21 | _dataSourceFunc = value; 22 | _updateData?.Invoke(); 23 | } 24 | } 25 | 26 | // This method is called when an instance of a view model inside this view model's scope is being created. 27 | // It provides a chance for this view model to initialize them. 28 | public override void OnSubVMCreated(BaseVM subVM) 29 | { 30 | if (subVM is MovieTableVM) 31 | InitMovieTableVM(subVM as MovieTableVM); 32 | else if (subVM is MovieFilterVM) 33 | InitMovieFilterVM(subVM as MovieFilterVM); 34 | } 35 | 36 | private void InitMovieTableVM(MovieTableVM vm) 37 | { 38 | // Forward the movie table's Selected event. 39 | vm.Selected += (sender, key) => Selected?.Invoke(this, key); 40 | 41 | // Create an action to update the movie table with the filtered data. 42 | _updateData = () => vm.DataSource = GetFilteredData; 43 | _updateData(); 44 | } 45 | 46 | private void InitMovieFilterVM(MovieFilterVM vm) 47 | { 48 | // If a filter is added, set the filter query and update the movie table data. 49 | vm.FilterChanged += (sender, query) => 50 | { 51 | _query = query; 52 | _updateData?.Invoke(); 53 | }; 54 | } 55 | 56 | private IEnumerable GetFilteredData() 57 | { 58 | try 59 | { 60 | return !string.IsNullOrEmpty(_query) ? 61 | _dataSourceFunc().AsQueryable().Where(_query) : _dataSourceFunc(); 62 | } 63 | catch (Exception) 64 | { 65 | return new List(); 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/CompositeView/MovieDetailsVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotNetify; 3 | 4 | namespace Website.Server 5 | { 6 | public class MovieDetailsVM : BaseVM 7 | { 8 | private readonly IMovieService _movieService; 9 | 10 | public MovieRecord Movie 11 | { 12 | get { return Get(); } 13 | set { Set(value); } 14 | } 15 | 16 | public MovieDetailsVM(IMovieService movieService) 17 | { 18 | _movieService = movieService; 19 | } 20 | 21 | public void SetByAFIRank(int rank) => Movie = _movieService.GetMovieByAFIRank(rank); 22 | } 23 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/CompositeView/MovieFilterVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using DotNetify; 5 | 6 | namespace Website.Server 7 | { 8 | public class MovieFilterVM : BaseVM 9 | { 10 | private List _filters = new List(); 11 | 12 | public class MovieFilter 13 | { 14 | public int Id { get; set; } 15 | public string Property { get; set; } 16 | public string Operation { get; set; } 17 | public string Text { get; set; } 18 | 19 | public string ToQuery() 20 | { 21 | /* Cast is a reserved query word */ 22 | if (Property == "Cast") 23 | Property = "MovieCast"; 24 | 25 | if (Operation == "contains") 26 | return Property == "Any" ? $"( Movie + MovieCast + Director ).toLower().contains(\"{Text.ToLower()}\")" 27 | : $"{Property}.toLower().contains(\"{Text.ToLower()}\")"; 28 | else 29 | { 30 | int intValue = int.Parse(Text); 31 | if (Operation == "equals") 32 | return $"{Property} == {intValue}"; 33 | else 34 | return $"{Property} {Operation} {intValue}"; 35 | } 36 | } 37 | 38 | public static string BuildQuery(IEnumerable filters) => string.Join(" and ", filters.Select(i => i.ToQuery())); 39 | } 40 | 41 | public Action Apply => arg => 42 | { 43 | _filters.Add(arg); 44 | FilterChanged?.Invoke(this, MovieFilter.BuildQuery(_filters)); 45 | }; 46 | 47 | public Action Delete => id => 48 | { 49 | _filters = _filters.Where(i => i.Id != id).ToList(); 50 | FilterChanged?.Invoke(this, MovieFilter.BuildQuery(_filters)); 51 | }; 52 | 53 | public event EventHandler FilterChanged; 54 | } 55 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/CompositeView/MovieTableVM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using DotNetify; 5 | 6 | namespace Website.Server 7 | { 8 | public interface IPaginatedTable 9 | { 10 | IEnumerable Headers { get; } 11 | IEnumerable Data { get; } 12 | int SelectedKey { get; set; } 13 | int[] Pagination { get; } 14 | int SelectedPage { get; set; } 15 | } 16 | 17 | public class MovieTableVM : BaseVM, IPaginatedTable 18 | { 19 | private int _recordsPerPage = 10; 20 | private Func> _dataSourceFunc; 21 | 22 | public IEnumerable Headers => new string[] { "Rank", "Movie", "Year", "Director" }; 23 | 24 | public Func> DataSource 25 | { 26 | set 27 | { 28 | _dataSourceFunc = value; 29 | Changed(nameof(Data)); 30 | } 31 | } 32 | 33 | public IEnumerable Data => GetData(); 34 | 35 | public int SelectedKey 36 | { 37 | get => Get(); 38 | set 39 | { 40 | Set(value); 41 | Selected?.Invoke(this, value); 42 | } 43 | } 44 | 45 | public int[] Pagination 46 | { 47 | get => Get(); 48 | set 49 | { 50 | Set(value); 51 | SelectedPage = 1; 52 | } 53 | } 54 | 55 | public int SelectedPage 56 | { 57 | get => Get(); 58 | set 59 | { 60 | Set(value); 61 | Changed(nameof(Data)); 62 | } 63 | } 64 | 65 | public event EventHandler Selected; 66 | 67 | private IEnumerable GetData() 68 | { 69 | if (_dataSourceFunc == null) 70 | return null; 71 | 72 | var data = _dataSourceFunc(); 73 | if (!data.Any(i => i.Rank == SelectedKey)) 74 | SelectedKey = data.Count() > 0 ? data.First().Rank : -1; 75 | 76 | return Paginate(data); 77 | } 78 | 79 | private IEnumerable Paginate(IEnumerable data) 80 | { 81 | // ChangedProperties is a base class property that contains a list of changed properties. 82 | // Here it's used to check whether user has changed the SelectedPage property value by clicking a pagination button. 83 | if (this.HasChanged(nameof(SelectedPage))) 84 | return data.Skip(_recordsPerPage * (SelectedPage - 1)).Take(_recordsPerPage); 85 | else 86 | { 87 | var pageCount = (int)Math.Ceiling(data.Count() / (double)_recordsPerPage); 88 | Pagination = Enumerable.Range(1, pageCount).ToArray(); 89 | return data.Take(_recordsPerPage); 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/HelloWorld.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | 3 | namespace Website.Server 4 | { 5 | public class HelloWorldVM : BaseVM 6 | { 7 | private string _firstName = "Hello"; 8 | private string _lastName = "World"; 9 | 10 | public string FirstName 11 | { 12 | get => _firstName; 13 | set 14 | { 15 | _firstName = value; 16 | Changed(nameof(FullName)); 17 | } 18 | } 19 | 20 | public string LastName 21 | { 22 | get => _lastName; 23 | set 24 | { 25 | _lastName = value; 26 | Changed(nameof(FullName)); 27 | } 28 | } 29 | 30 | public string FullName => $"{FirstName} {LastName}"; 31 | } 32 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/LiveChart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using DotNetify; 5 | 6 | namespace Website.Server 7 | { 8 | public class LiveChartVM : BaseVM 9 | { 10 | public string[][] Waveform 11 | { 12 | get => Get(); 13 | set => Set(value); 14 | } 15 | 16 | public int[] Bar 17 | { 18 | get => Get(); 19 | set => Set(value); 20 | } 21 | 22 | public double[] Pie 23 | { 24 | get => Get(); 25 | set => Set(value); 26 | } 27 | 28 | public LiveChartVM() 29 | { 30 | var timer = Observable.Interval(TimeSpan.FromSeconds(1)); 31 | var random = new Random(); 32 | 33 | Waveform = Enumerable.Range(1, 30).Select(x => new string[] { $"{x}", $"{Math.Sin(x / Math.PI)}" }).ToArray(); 34 | Bar = Enumerable.Range(1, 8).Select(_ => random.Next(500, 1000)).ToArray(); 35 | Pie = Enumerable.Range(1, 3).Select(_ => random.NextDouble()).ToArray(); 36 | 37 | timer.Subscribe(x => 38 | { 39 | x += 31; 40 | this.AddList(nameof(Waveform), new string[] { $"{x}", $"{Math.Sin(x / Math.PI)}" }); 41 | 42 | Bar = Enumerable.Range(1, 12).Select(_ => random.Next(500, 1000)).ToArray(); 43 | Pie = Enumerable.Range(1, 3).Select(_ => random.NextDouble()).ToArray(); 44 | 45 | PushUpdates(); 46 | }); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/MasterDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using DotNetify; 5 | 6 | namespace Website.Server 7 | { 8 | public class MasterDetails : BaseVM 9 | { 10 | private readonly IWebStoreService _webStoreService; 11 | 12 | private event EventHandler SelectedItem; 13 | 14 | public MasterDetails(IWebStoreService webStoreService) 15 | { 16 | _webStoreService = webStoreService; 17 | } 18 | 19 | public override void OnSubVMCreated(BaseVM vm) 20 | { 21 | if (vm is Master) 22 | { 23 | var master = vm as Master; 24 | master.Selected += (sender, id) => SelectedItem?.Invoke(this, id); 25 | } 26 | else if (vm is Details) 27 | { 28 | var detail = vm as Details; 29 | SelectedItem += (sender, id) => 30 | { 31 | detail.SetData(_webStoreService.GetBookByIdAsync(id).Result); 32 | }; 33 | } 34 | } 35 | } 36 | 37 | public class Master : BaseVM 38 | { 39 | private readonly IWebStoreService _webStoreService; 40 | 41 | public IEnumerable ListItems 42 | { 43 | get => Get>(); 44 | set => Set(value); 45 | } 46 | 47 | public event EventHandler Selected; 48 | 49 | public Master(IWebStoreService webStoreService) 50 | { 51 | _webStoreService = webStoreService; 52 | } 53 | 54 | public override async Task OnCreatedAsync() 55 | { 56 | ListItems = await _webStoreService.GetAllBooksAsync(); 57 | } 58 | 59 | public void Select(int id) => Selected?.Invoke(this, id); 60 | } 61 | 62 | public class Details : BaseVM 63 | { 64 | public string ItemImageUrl 65 | { 66 | get => Get(); 67 | set => Set(value); 68 | } 69 | 70 | public void SetData(WebStoreRecord data) => ItemImageUrl = data.ImageUrl; 71 | } 72 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/Overview.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using DotNetify; 4 | 5 | namespace Website.Server 6 | { 7 | public class RealTimePush : BaseVM 8 | { 9 | private Timer _timer; 10 | public string Greetings => "Hello World!"; 11 | public DateTime ServerTime => DateTime.Now; 12 | 13 | public RealTimePush() 14 | { 15 | _timer = new Timer(state => 16 | { 17 | Changed(nameof(ServerTime)); 18 | PushUpdates(); 19 | }, null, 0, 1000); // every 1000 ms. 20 | } 21 | 22 | public override void Dispose() => _timer.Dispose(); 23 | } 24 | 25 | public class ServerUpdate : BaseVM 26 | { 27 | public class Person 28 | { 29 | public string FirstName { get; set; } 30 | public string LastName { get; set; } 31 | } 32 | 33 | public string Greetings { get; set; } = "Hello World!"; 34 | 35 | public void Submit(Person person) 36 | { 37 | Greetings = $"Hello {person.FirstName} {person.LastName}!"; 38 | Changed(nameof(Greetings)); 39 | } 40 | } 41 | 42 | public class TwoWayBinding : BaseVM 43 | { 44 | public string Greetings => $"Hello {Name}"; 45 | 46 | private string _name = "World"; 47 | 48 | public string Name 49 | { 50 | get => _name; 51 | set 52 | { 53 | _name = value; 54 | Changed(nameof(Greetings)); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/SecurePage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using DotNetify; 4 | using DotNetify.Security; 5 | using Microsoft.IdentityModel.Tokens; 6 | 7 | namespace Website.Server 8 | { 9 | public class SetAccessTokenAttribute : Attribute { } 10 | 11 | [Authorize] 12 | [SetAccessToken] 13 | public class SecurePageVM : BaseVM 14 | { 15 | private Timer _timer; 16 | private SecurityToken _accessToken; 17 | private readonly IPrincipalAccessor _principalAccessor; 18 | 19 | private int AccessExpireTime => (int) (_accessToken.ValidTo - DateTime.UtcNow).TotalSeconds; 20 | 21 | public string SecureCaption { get; set; } 22 | public string SecureData { get; set; } 23 | 24 | public SecurePageVM(IPrincipalAccessor principalAccessor) 25 | { 26 | _principalAccessor = principalAccessor; 27 | } 28 | 29 | public override void Dispose() => _timer?.Dispose(); 30 | 31 | public void SetAccessToken(SecurityToken accessToken) 32 | { 33 | if (_accessToken?.ValidTo != accessToken.ValidTo) 34 | { 35 | _accessToken = accessToken; 36 | SecureCaption = $"Authenticated user: \"{_principalAccessor.Principal?.Identity.Name}\""; 37 | Changed(nameof(SecureCaption)); 38 | 39 | // IMPORTANT: Create new timer if access token changes to make sure the timer thread uses 40 | // the new hub caller context with the updated claims principal from the new token. 41 | _timer?.Dispose(); 42 | _timer = new Timer(state => 43 | { 44 | SecureData = _accessToken != null ? $"Access token will expire in {AccessExpireTime} seconds" : null; 45 | Changed(nameof(SecureData)); 46 | PushUpdates(); 47 | }, null, 0, 1000); 48 | } 49 | } 50 | } 51 | 52 | [Authorize(Role = "admin")] 53 | [SetAccessToken] 54 | public class AdminSecurePageVM : BaseVM 55 | { 56 | public string TokenIssuer { get; set; } 57 | public string TokenValidFrom { get; set; } 58 | public string TokenValidTo { get; set; } 59 | 60 | public void Refresh() 61 | { 62 | /* no op */ 63 | } 64 | 65 | public void SetAccessToken(SecurityToken accessToken) 66 | { 67 | TokenIssuer = $"Token issuer: \"{accessToken.Issuer}\""; 68 | TokenValidFrom = $"Valid from: {accessToken.ValidFrom:R}"; 69 | TokenValidTo = $"Valid to: {accessToken.ValidTo:R}"; 70 | 71 | Changed(nameof(TokenIssuer)); 72 | Changed(nameof(TokenValidFrom)); 73 | Changed(nameof(TokenValidTo)); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/Examples/SimpleList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Threading.Tasks; 6 | using DotNetify; 7 | using DotNetify.Elements; 8 | 9 | namespace Website.Server 10 | { 11 | public class SimpleListVM : MulticastVM 12 | { 13 | private readonly IEmployeeRepository _repository; 14 | private readonly IConnectionContext _connectionContext; 15 | 16 | public class EmployeeInfo 17 | { 18 | public int Id { get; set; } 19 | public string FirstName { get; set; } 20 | public string LastName { get; set; } 21 | } 22 | 23 | // If you use CRUD methods on a list, you must set the item key prop name of that list with ItemKey attribute. 24 | [ItemKey(nameof(Employee.Id))] 25 | public IEnumerable Employees { get; private set; } 26 | 27 | // Clients from the same IP address will share the same VM instance. 28 | public override string GroupName => _connectionContext.HttpConnection.LocalIpAddress.ToString(); 29 | 30 | public SimpleListVM(IEmployeeRepository repository, IConnectionContext connectionContext) 31 | { 32 | _repository = repository; 33 | _connectionContext = connectionContext; 34 | } 35 | 36 | public override async Task OnCreatedAsync() 37 | { 38 | Employees = (await _repository.GetAllAsync(7)) 39 | .Select(i => new EmployeeInfo { Id = i.Id, FirstName = i.FirstName, LastName = i.LastName }); 40 | } 41 | 42 | public async Task Add(string fullName) 43 | { 44 | var names = fullName.Split(new char[] { ' ' }, 2); 45 | var employee = new Employee 46 | { 47 | FirstName = names.First(), 48 | LastName = names.Length > 1 ? names.Last() : "" 49 | }; 50 | 51 | // Use CRUD base method to add the list item on the client. 52 | this.AddList("Employees", new EmployeeInfo 53 | { 54 | Id = await _repository.AddAsync(employee), 55 | FirstName = employee.FirstName, 56 | LastName = employee.LastName 57 | }); 58 | } 59 | 60 | public async Task Update(EmployeeInfo employeeInfo) 61 | { 62 | var employee = await _repository.GetAsync(employeeInfo.Id); 63 | if (employee != null) 64 | { 65 | employee.FirstName = employeeInfo.FirstName ?? employee.FirstName; 66 | employee.LastName = employeeInfo.LastName ?? employee.LastName; 67 | await _repository.UpdateAsync(employee); 68 | 69 | this.UpdateList(nameof(Employees), new EmployeeInfo 70 | { 71 | Id = employee.Id, 72 | FirstName = employee.FirstName, 73 | LastName = employee.LastName 74 | }); 75 | } 76 | } 77 | 78 | public async Task Remove(int id) 79 | { 80 | await _repository.RemoveAsync(id); 81 | 82 | // Use CRUD base method to remove the list item on the client. 83 | this.RemoveList(nameof(Employees), id); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Dashboard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using DotNetify; 6 | using DotNetify.Elements; 7 | using DotNetify.Routing; 8 | 9 | namespace Website.Server 10 | { 11 | public class Dashboard : BaseVM, IRoutable 12 | { 13 | private IDisposable _subscription; 14 | 15 | public RoutingState RoutingState { get; set; } = new RoutingState(); 16 | 17 | public Dashboard(ILiveDataService liveDataService) 18 | { 19 | AddProperty("Download") 20 | .WithAttribute(new { Label = "Download", Icon = "cloud_download" }) 21 | .SubscribeTo(liveDataService.Download); 22 | 23 | AddProperty("Upload") 24 | .WithAttribute(new { Label = "Upload", Icon = "cloud_upload" }) 25 | .SubscribeTo(liveDataService.Upload); 26 | 27 | AddProperty("Latency") 28 | .WithAttribute(new { Label = "Latency", Icon = "network_check" }) 29 | .SubscribeTo(liveDataService.Latency); 30 | 31 | AddProperty("Users") 32 | .WithAttribute(new { Label = "Users", Icon = "face" }) 33 | .SubscribeTo(liveDataService.Users); 34 | 35 | AddProperty("Traffic").SubscribeTo(liveDataService.Traffic); 36 | 37 | AddProperty("Utilization") 38 | .WithAttribute(new ChartAttribute { Labels = new string[] { "Memory", "Disk", "Network" } }) 39 | .SubscribeTo(liveDataService.Utilization); 40 | 41 | AddProperty("ServerUsage").SubscribeTo(liveDataService.ServerUsage) 42 | .WithAttribute(new ChartAttribute { Labels = new string[] { "dns", "sql", "nethst", "w2k", "ubnt", "uat", "ftp", "smtp", "exch", "demo" } }); 43 | 44 | AddProperty("RecentActivities") 45 | .SubscribeTo(liveDataService.RecentActivity.Select(value => 46 | { 47 | var activities = new Queue(Get("RecentActivities")?.Reverse() ?? new Activity[] { }); 48 | activities.Enqueue(value); 49 | if (activities.Count > 4) 50 | activities.Dequeue(); 51 | 52 | return activities.Reverse().ToArray(); 53 | })); 54 | 55 | // Regulate data update interval to no less than every 200 msecs. 56 | _subscription = Observable 57 | .Interval(TimeSpan.FromMilliseconds(200)) 58 | .StartWith(0) 59 | .Subscribe(_ => PushUpdates()); 60 | } 61 | 62 | public override void Dispose() 63 | { 64 | _subscription?.Dispose(); 65 | base.Dispose(); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/AddressForm.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reactive.Linq; 3 | using DotNetify; 4 | using DotNetify.Elements; 5 | 6 | namespace Website.Server 7 | { 8 | public class AddressForm : BaseVM 9 | { 10 | public ReactiveProperty Customer { get; } = new ReactiveProperty(); 11 | 12 | public AddressForm() 13 | { 14 | AddProperty(nameof(AddressInfo.Address1)) 15 | .WithAttribute(new TextFieldAttribute { Label = "Address 1:" }) 16 | .SubscribeTo(Customer.Select(x => x.Address.Address1)); 17 | 18 | AddProperty(nameof(AddressInfo.Address2)) 19 | .WithAttribute(new TextFieldAttribute { Label = "Address 2:" }) 20 | .SubscribeTo(Customer.Select(x => x.Address.Address2)); 21 | 22 | AddProperty(nameof(AddressInfo.City)) 23 | .WithAttribute(new TextFieldAttribute { Label = "City:" }) 24 | .SubscribeTo(Customer.Select(x => x.Address.City)); 25 | 26 | AddProperty(nameof(AddressInfo.State)) 27 | .WithAttribute(new DropdownListAttribute 28 | { 29 | Label = "State:", 30 | Options = typeof(State).ToDescriptions() 31 | }) 32 | .SubscribeTo(Customer.Select(x => x.Address.State)); 33 | 34 | AddProperty(nameof(AddressInfo.ZipCode)) 35 | .WithAttribute(new TextFieldAttribute { Label = "Zip Code:" }) 36 | .SubscribeTo(Customer.Select(x => x.Address.ZipCode)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/CustomerForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reactive.Linq; 4 | using DotNetify; 5 | using DotNetify.Elements; 6 | using DotNetify.Security; 7 | 8 | namespace Website.Server 9 | { 10 | public class CustomerForm : BaseVM 11 | { 12 | private readonly ICustomerRepository _customerRepository; 13 | private readonly ReactiveProperty _selectedContact; 14 | 15 | public class Contact 16 | { 17 | public int Id { get; set; } 18 | public string Name { get; set; } 19 | public string Phone { get; set; } 20 | public string Address { get; set; } 21 | public string City { get; set; } 22 | public string ZipCode { get; set; } 23 | } 24 | 25 | public CustomerForm(ICustomerRepository customerRepository) 26 | { 27 | _customerRepository = customerRepository; 28 | 29 | _selectedContact = AddProperty("SelectedContact", 1); 30 | 31 | AddProperty("Contacts", customerRepository.GetAll().Select(customer => ToContact(customer))) 32 | .WithItemKey(nameof(Contact.Id)) 33 | .WithAttribute(new DataGridAttribute 34 | { 35 | RowKey = nameof(Contact.Id), 36 | Columns = new DataGridColumn[] { 37 | new DataGridColumn(nameof(Contact.Name), "Name") { Sortable = true }, 38 | new DataGridColumn(nameof(Contact.Phone), "Phone") { Sortable = true }, 39 | new DataGridColumn(nameof(Contact.Address), "Address") { Sortable = true }, 40 | new DataGridColumn(nameof(Contact.City), "City") { Sortable = true }, 41 | new DataGridColumn(nameof(Contact.ZipCode), "ZipCode") { Sortable = true } 42 | }, 43 | Rows = 5 44 | }.CanSelect(DataGridAttribute.Selection.Single, _selectedContact)); 45 | 46 | AddInternalProperty("Submit") 47 | .SubscribedBy(AddProperty("SubmitSuccess"), formData => Save(formData)); 48 | } 49 | 50 | public override void OnSubVMCreated(BaseVM subVM) 51 | { 52 | // Have sub-forms with 'Customer' property subscribe to the customer data grid's selection changed event. 53 | var customerPropInfo = subVM.GetType().GetProperty(nameof(Customer)); 54 | if (typeof(ReactiveProperty).IsAssignableFrom(customerPropInfo?.PropertyType)) 55 | _selectedContact.SubscribedBy( 56 | customerPropInfo.GetValue(subVM) as ReactiveProperty, 57 | id => _customerRepository.Get(id) 58 | ); 59 | 60 | if (subVM is NewCustomerForm) 61 | (subVM as NewCustomerForm).NewCustomer.Subscribe(customer => UpdateContact(customer)); 62 | } 63 | 64 | private bool Save(CustomerFormData formData) 65 | { 66 | var id = (int) _selectedContact.Value; 67 | var customer = _customerRepository.Update(id, formData); 68 | 69 | this.UpdateList("Contacts", ToContact(customer)); 70 | _selectedContact.Value = id; 71 | return true; 72 | } 73 | 74 | private Contact ToContact(Customer customer) => new Contact 75 | { 76 | Id = customer.Id, 77 | Name = customer.Name.FullName, 78 | Address = customer.Address.StreetAddress, 79 | City = customer.Address.City, 80 | ZipCode = customer.Address.ZipCode, 81 | Phone = customer.Phone.PrimaryNumber 82 | }; 83 | 84 | private void UpdateContact(Customer newCustomer) 85 | { 86 | this.AddList("Contacts", ToContact(newCustomer)); 87 | _selectedContact.OnNext(newCustomer.Id); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/CustomerFormData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Website.Server 4 | { 5 | using StringDictionary = Dictionary; 6 | 7 | public class CustomerFormData 8 | { 9 | public StringDictionary Person { get; set; } 10 | public StringDictionary Phone { get; set; } 11 | public StringDictionary Address { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/NewCustomerForm.cs: -------------------------------------------------------------------------------- 1 | using DotNetify; 2 | using DotNetify.Elements; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | 6 | namespace Website.Server 7 | { 8 | public class NewCustomerForm : BaseVM 9 | { 10 | private readonly ICustomerRepository _customerRepository; 11 | 12 | public ReactiveProperty NewCustomer { get; } = new ReactiveProperty(); 13 | 14 | public NewCustomerForm(ICustomerRepository customerRepository) 15 | { 16 | _customerRepository = customerRepository; 17 | 18 | AddInternalProperty("Submit") 19 | .SubscribedBy(NewCustomer, formData => Save(formData)); 20 | } 21 | 22 | public override void Dispose() 23 | { 24 | base.Dispose(); 25 | } 26 | 27 | public Customer Save(CustomerFormData formData) 28 | { 29 | return _customerRepository.Add(formData); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/PersonForm.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reactive.Linq; 3 | using DotNetify; 4 | using DotNetify.Elements; 5 | 6 | namespace Website.Server 7 | { 8 | public class PersonForm : BaseVM 9 | { 10 | public ReactiveProperty Customer { get; } = new ReactiveProperty(); 11 | 12 | public PersonForm() 13 | { 14 | AddProperty(nameof(NameInfo.FullName)) 15 | .WithAttribute(new TextFieldAttribute { Label = "Name:" }) 16 | .SubscribeTo(Customer.Select(x => x.Name.FullName)); 17 | 18 | AddProperty(nameof(NameInfo.Prefix)) 19 | .WithAttribute(new DropdownListAttribute { Label = "Prefix:", Options = typeof(NamePrefix).ToDescriptions() }) 20 | .SubscribeTo(Customer.Select(x => x.Name.Prefix)); 21 | 22 | AddProperty(nameof(NameInfo.FirstName)) 23 | .WithAttribute(new TextFieldAttribute { Label = "First Name:", MaxLength = 35 }) 24 | .WithRequiredValidation() 25 | .SubscribeTo(Customer.Select(x => x.Name.FirstName)); 26 | 27 | AddProperty(nameof(NameInfo.MiddleName)) 28 | .WithAttribute(new TextFieldAttribute { Label = "Middle Name:", MaxLength = 35 }) 29 | .SubscribeTo(Customer.Select(x => x.Name.MiddleName)); 30 | 31 | AddProperty(nameof(NameInfo.LastName)) 32 | .WithAttribute(new TextFieldAttribute { Label = "Last Name:", MaxLength = 35 }) 33 | .WithRequiredValidation() 34 | .SubscribeTo(Customer.Select(x => x.Name.LastName)); 35 | 36 | AddProperty(nameof(NameInfo.Suffix)) 37 | .WithAttribute(new DropdownListAttribute { Label = "Suffix:", Options = typeof(NameSuffix).ToDescriptions() }) 38 | .SubscribeTo(Customer.Select(x => x.Name.Suffix)); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/PhoneForm.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reactive.Linq; 3 | using DotNetify; 4 | using DotNetify.Elements; 5 | 6 | namespace Website.Server 7 | { 8 | public class PhoneForm : BaseVM 9 | { 10 | public ReactiveProperty Customer { get; } = new ReactiveProperty(); 11 | 12 | public PhoneForm() 13 | { 14 | AddProperty(nameof(PhoneInfo.Work)) 15 | .WithAttribute(new TextFieldAttribute { Label = "Work:", Mask = "(999) 999-9999" }) 16 | .WithPatternValidation(Pattern.USPhoneNumber) 17 | .SubscribeTo(Customer.Select(x => x.Phone.Work)); 18 | 19 | AddProperty(nameof(PhoneInfo.Home)) 20 | .WithAttribute(new TextFieldAttribute { Label = "Home:", Mask = "(999) 999-9999" }) 21 | .WithPatternValidation(Pattern.USPhoneNumber) 22 | .SubscribeTo(Customer.Select(x => x.Phone.Home)); 23 | 24 | AddProperty(nameof(PhoneInfo.Mobile)) 25 | .WithAttribute(new TextFieldAttribute { Label = "Mobile:", Mask = "(999) 999-9999" }) 26 | .WithPatternValidation(Pattern.USPhoneNumber) 27 | .SubscribeTo(Customer.Select(x => x.Phone.Mobile)); 28 | 29 | AddProperty(nameof(PhoneInfo.Primary)) 30 | .WithAttribute(new DropdownListAttribute { Label = "Primary Phone:", Options = typeof(PrimaryPhone).ToDescriptions() }) 31 | .SubscribeTo(Customer.Select(x => x.Phone.Primary)); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Website/Website.Server/ViewModels/ExamplesWithElements/Form/Services/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Threading; 7 | 8 | namespace Website.Server 9 | { 10 | using StringDictionary = Dictionary; 11 | 12 | public interface ICustomerRepository 13 | { 14 | IEnumerable GetAll(); 15 | 16 | Customer Get(int id); 17 | 18 | Customer Add(CustomerFormData formData); 19 | 20 | Customer Update(int id, CustomerFormData formData); 21 | } 22 | 23 | public class CustomerRepository : ICustomerRepository 24 | { 25 | private IList _mockData = GenerateMockData(); 26 | 27 | public IEnumerable GetAll() => _mockData; 28 | 29 | public Customer Get(int id) => _mockData.FirstOrDefault(x => x.Id == id); 30 | 31 | public Customer Add(CustomerFormData formData) 32 | { 33 | var customer = new Customer 34 | { 35 | Id = _mockData.Max(x => x.Id) + 1, 36 | Name = new NameInfo(), 37 | Address = new AddressInfo(), 38 | Phone = new PhoneInfo() 39 | }; 40 | 41 | Update(customer.Name, formData.Person); 42 | Update(customer.Address, formData.Address); 43 | Update(customer.Phone, formData.Phone); 44 | 45 | _mockData.Add(customer); 46 | return customer; 47 | } 48 | 49 | public Customer Update(int id, CustomerFormData formData) 50 | { 51 | var customer = Get(id); 52 | 53 | Update(customer.Name, formData.Person); 54 | Update(customer.Address, formData.Address); 55 | Update(customer.Phone, formData.Phone); 56 | return customer; 57 | } 58 | 59 | private void Update(object record, StringDictionary newValues) 60 | { 61 | if (newValues != null) 62 | foreach (var prop in record.GetType().GetProperties().Where(prop => newValues.ContainsKey(prop.Name))) 63 | prop.SetValue(record, TypeDescriptor.GetConverter(prop.PropertyType).ConvertFromString(newValues[prop.Name])); 64 | } 65 | 66 | private static IList GenerateMockData() 67 | { 68 | int id = 0; 69 | return new Faker() 70 | .CustomInstantiator(f => new Customer { Id = ++id }) 71 | .RuleFor(o => o.Name, f => new NameInfo 72 | { 73 | FirstName = f.Person.FirstName, 74 | LastName = f.Person.LastName, 75 | }) 76 | .RuleFor(o => o.Address, f => new AddressInfo 77 | { 78 | Address1 = f.Address.StreetAddress(), 79 | Address2 = f.Address.SecondaryAddress(), 80 | City = f.Address.City(), 81 | State = (State) Enum.Parse(typeof(State), f.Address.StateAbbr()), 82 | ZipCode = f.Address.ZipCode("#####") 83 | }) 84 | .RuleFor(o => o.Phone, f => new PhoneInfo 85 | { 86 | Work = f.Phone.PhoneNumber("(###) ###-####"), 87 | Primary = f.PickRandomWithout(PrimaryPhone.None), 88 | }) 89 | .Generate(100); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Website/Website.Server/Website.Server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Website/Website.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Aws": { 3 | "Region": "", 4 | "AccessKeyId": "", 5 | "SecretAccessKey": "", 6 | "ConnectionUrl": "" 7 | }, 8 | "WSServer": { 9 | "ConnectionUrl": "" 10 | } 11 | } -------------------------------------------------------------------------------- /Website/Website.Server/pulse-ui/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Pulse 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |

    Pulse

    39 |
    40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 |
    48 |
    49 | 50 | 51 | -------------------------------------------------------------------------------- /Website/Website.Server/pulse-ui/section.html: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Website/Website.Server/pulse-ui/section_template.html: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Website/Website.Server/pulse-ui/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 14px; 3 | } 4 | 5 | label { 6 | font-size: small; 7 | } 8 | 9 | .card { 10 | padding: 1rem 11 | } 12 | /*style_ext*/ -------------------------------------------------------------------------------- /deploy.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if "%1"=="local" goto :local 3 | goto :heroku 4 | 5 | :local 6 | echo --- Remove any existing image 7 | docker rmi blazor -f 8 | 9 | echo --- Build a new image 10 | docker build -t blazor -f ./Dockerfile . --build-arg aspnetenv=Production 11 | 12 | echo --- Remove build images 13 | docker image prune -f --filter label=stage=build 14 | rd __tmp__ /q /s 15 | 16 | echo --- Run a container on port 8090 17 | docker run -it --rm -p:8090:80 --name blazor_8090 blazor 18 | 19 | goto :end 20 | 21 | :heroku 22 | rem heroku login 23 | call heroku container:login 24 | call heroku container:push web -a dotnetify-blazor 25 | call heroku container:release web -a dotnetify-blazor 26 | 27 | :end --------------------------------------------------------------------------------