Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input Large Text Area #380

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/InputLargeTextArea/InputLargeTextArea.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31612.314
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InputLargeTextArea", "src\InputLargeTextArea\InputLargeTextArea.csproj", "{D3C10569-1320-4C66-B76B-FC02BDC9010F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleServerApp", "SampleServerApp\SampleServerApp.csproj", "{8162C3A5-8054-4647-B597-64A01027EC24}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D3C10569-1320-4C66-B76B-FC02BDC9010F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D3C10569-1320-4C66-B76B-FC02BDC9010F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3C10569-1320-4C66-B76B-FC02BDC9010F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3C10569-1320-4C66-B76B-FC02BDC9010F}.Release|Any CPU.Build.0 = Release|Any CPU
{8162C3A5-8054-4647-B597-64A01027EC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8162C3A5-8054-4647-B597-64A01027EC24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8162C3A5-8054-4647-B597-64A01027EC24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8162C3A5-8054-4647-B597-64A01027EC24}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {130EF2A6-BAD0-430B-9B31-D35406A46F67}
EndGlobalSection
EndGlobal
43 changes: 43 additions & 0 deletions src/InputLargeTextArea/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Blazor `InputLargeTextArea` Component Sample
A multiline input component for Blazor Server to enable editing large string values. Supports async content access without binding and without validations.

### Example:
```csharp
<InputLargeTextArea id="largeTextArea" @ref="TextArea" OnChange="TextAreaChanged" />


@code {
InputLargeTextArea? TextArea;

public async Task GetTextAsync()
{
var streamReader = await TextArea!.GetTextAsync(maxLength: 50_000);
var textFromInputLargeTextArea = await streamReader.ReadToEndAsync();
}

public async Task SetTextAsync()
{
var textToWrite = new string('c', 50_000);

var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream);
await streamWriter.WriteAsync(textToWrite);
await streamWriter.FlushAsync();
await TextArea!.SetTextAsync(streamWriter);
}

public void TextAreaChanged(InputLargeTextAreaChangeEventArgs args)
{
LastChangedLength = args.Length;
}
}
```

## Why?
Using Blazor Server's `InputTextArea` with large (ex. 20K chars) amounts of text can lead to a degraded user experience due to the constant round-trip communication to/from the server to enable binding and validations. This component provides an asynchronous ability to get & set the text area content. This approach **is not optimal** due to the additional complexity working with `StreamReader`/`StreamWriter` APIs, as well as the (large) amount of memory allocations which may occur when encoding/decoding the `UTF-8` `string`/`textarea` content into `byte`s. Due to these concerns, we've made this available as a sample instead of adding it to the core framework.

Note: If you're encountering slowdowns specifically in complex components or Blazor WebAssembly, we recommend reviewing the [Blazor WebAssembly Performance Best Practices](https://docs.microsoft.com/en-us/aspnet/core/blazor/webassembly-performance-best-practices?view=aspnetcore-6.0#avoid-rerendering-after-handling-events-without-state-changes) which detail rendering optimizations.

## Setup
1. Add a package reference to `InputLargeTextArea`.
2. Add `<script src="_content/InputLargeTextArea/js/InputLargeTextArea.js"></script>` to your `_Layout.cshtml` on Blazor Server.
12 changes: 12 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
42 changes: 42 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Pages/Error.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@page
@model SampleServerApp.Pages.ErrorModel
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure if I should keep the basic template pages or remove?


<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Error</title>
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
</head>

<body>
<div class="main">
<div class="content px-4">
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
</div>
</div>
</body>

</html>
26 changes: 26 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Pages/Error.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SampleServerApp.Pages;

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

private readonly ILogger<ErrorModel> _logger;

public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}

public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
50 changes: 50 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@page "/"
@using Microsoft.AspNetCore.Components.Forms

<PageTitle>Input Large Text Area</PageTitle>

<InputLargeTextArea id="largeTextArea" @ref="TextArea" OnChange="TextAreaChanged" />

<br />

<button id="setTextBtn" @onclick="SetTextAsync">SetTextAsync</button>
<button id="getTextBtn" @onclick="GetTextAsync">GetTextAsync</button>

<hr />

<h3>Last Changed:</h3>
Length: <p id="lastChangedLength">@LastChangedLength</p>

<h3>Get Text Result:</h3>
<p id="getTextResult">@GetTextResult</p>
<p id="getTextError">@GetTextError</p>


@code {
public long LastChangedLength { get; set; }
public string GetTextResult { get; set; } = string.Empty;
public string GetTextError { get; set; } = string.Empty;

InputLargeTextArea? TextArea;

public async Task GetTextAsync()
{
var streamReader = await TextArea!.GetTextAsync(maxLength: 50_000);
GetTextResult = await streamReader.ReadToEndAsync();
StateHasChanged();
}

public async Task SetTextAsync()
{
var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream);
await streamWriter.WriteAsync(new string('c', 50_000));
await streamWriter.FlushAsync();
await TextArea!.SetTextAsync(streamWriter);
}

public void TextAreaChanged(InputLargeTextAreaChangeEventArgs args)
{
LastChangedLength = args.Length;
}
}
8 changes: 8 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Pages/_Host.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page "/"
@namespace SampleServerApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_Layout";
}

<component type="typeof(App)" render-mode="ServerPrerendered" />
33 changes: 33 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Pages/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@using Microsoft.AspNetCore.Components.Web
@namespace SampleServerApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="SampleServerApp.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()

<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

<script src="_content/InputLargeTextArea/js/InputLargeTextArea.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();


app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
13 changes: 13 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/SampleServerApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\src\InputLargeTextArea\InputLargeTextArea.csproj" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Shared/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@inherits LayoutComponentBase

<PageTitle>SampleServerApp</PageTitle>

<div class="page">
<div class="sidebar">
<NavMenu />
</div>

<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>

<article class="content px-4">
@Body
</article>
</main>
</div>
70 changes: 70 additions & 0 deletions src/InputLargeTextArea/SampleServerApp/Shared/MainLayout.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}

main {
flex: 1;
}

.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}

.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}

.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}

.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}

@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}

.top-row.auth {
justify-content: space-between;
}

.top-row a, .top-row .btn-link {
margin-left: 0;
}
}

@media (min-width: 641px) {
.page {
flex-direction: row;
}

.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}

.top-row {
position: sticky;
top: 0;
z-index: 1;
}

.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
Loading