Skip to content

Commit

Permalink
Merge pull request #113 from hydrostack/90-handle-iresult-return-type
Browse files Browse the repository at this point in the history
Handle IComponentResult
  • Loading branch information
kjeske authored Nov 7, 2024
2 parents ef10e79 + ad8d49a commit 2d5cd44
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 17 deletions.
54 changes: 54 additions & 0 deletions docs/content/features/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,60 @@ public class Counter : HydroComponent
</div>
```

## Results

Hydro provides multiple component results that can be returned from an action:

- `ComponentResults.Challenge`

Calls `HttpContext.ChallangeAsync` and handles further redirections

- `ComponentResults.SignIn`

Calls `HttpContext.SignInAsync` and handles further redirections

- `ComponentResults.SignOut`

Calls `HttpContext.SignOutAsync` and handles further redirections

- `ComponentResults.File`

Returns a file from the server



Examples:

```c#
// ShowInvoice.cshtml.cs
public class ShowInvoice : HydroComponent
{
public IComponentResult Download()
{
return ComponentResults.File("./storage/file.pdf", MediaTypeNames.Application.Pdf);
}
}
```

```c#
// Profile.cshtml.cs
public class Profile : HydroComponent
{
public IComponentResult LoginWithGitHub()
{
var properties = new AuthenticationProperties
{
RedirectUri = RedirectUri,
IsPersistent = true
};

return ComponentResults.Challenge(properties, [GitHubAuthenticationDefaults.AuthenticationScheme]);
}
}
```

## JavaScript expression as a parameter

In some cases, like integrating with JavaScript libraries like maps, rich-text editors, etc. it might be useful to
Expand Down
77 changes: 77 additions & 0 deletions docs/content/utilities/hydro-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,83 @@ Parameter names are converted to kebab-case when used as attributes on tags, so:
- `Message` property becomes `message` attribute.
- `StatusCode` property becomes `status-code` attribute.

## Passing handlers as parameters

When you need to pass a handler to a Hydro view, you can use the `Expression` type:

```c#
// FormButton.cshtml.cs
public class FormButton : HydroView
{
public Expression Click { get; set; }
}
```

```razor
<!-- FormButton.cshtml -->
@model FormButton
<button on:click="@Model.Click">
@Model.Slot()
</button>
```

Usage:

```razor
<form-button click="@(() => Model.Save())">
Save
</form-button>
```

## Calling actions of specific Hydro components

You can call an action of a specific Hydro component from a Hydro view using the `Reference<T>`. Example:

Parent (Hydro component):

```c#
public class Parent : HydroComponent
{
public string Value { get; set; }

public void LoadText(string value)
{
Value = value;
}
}
```

```razor
<!-- Parent.cshtml -->
@model Parent
<child-view />
```

Child (Hydro view):

```c#
public class ChildView : HydroView;
```

```razor
<!-- ChildView.cshtml -->
@model ChildView
@{
var parent = Model.Reference<Parent>();
}
<button on:click="@(() => parent.LoadText("Hello!"))">
Set text
</button>
```

## Dynamic attributes

All attributes passed to the Hydro view by the caller are available in a view definition, even when they are not defined as properties.
Expand Down
55 changes: 55 additions & 0 deletions src/ComponentResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Http;

namespace Hydro;

/// <summary>
/// Component result
/// </summary>
public interface IComponentResult
{
/// <summary>
/// Execute the result
/// </summary>
Task ExecuteAsync(HttpContext httpContext, HydroComponent component);
}

internal class ComponentResult : IComponentResult
{
private readonly IResult _result;
private readonly ComponentResultType _type;

internal ComponentResult(IResult result, ComponentResultType type)
{
_result = result;
_type = type;
}

public async Task ExecuteAsync(HttpContext httpContext, HydroComponent component)
{
var response = httpContext.Response;

response.Headers.TryAdd(HydroConsts.ResponseHeaders.SkipOutput, "True");

if (_type == ComponentResultType.File)
{
response.Headers.Append("Content-Disposition", "inline");
}

await _result.ExecuteAsync(httpContext);

if (response.Headers.Remove("Location", out var location))
{
response.StatusCode = StatusCodes.Status200OK;
component.Redirect(location);
}
}
}

internal enum ComponentResultType
{
Empty,
File,
Challenge,
SignIn,
SignOut
}
72 changes: 72 additions & 0 deletions src/ComponentResults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;

namespace Hydro;

/// <summary>
/// Results for Hydro actions
/// </summary>
public static class ComponentResults
{
/// <summary>
/// Create a ChallengeHttpResult
/// </summary>
public static IComponentResult Challenge(
AuthenticationProperties properties = null,
IList<string> authenticationSchemes = null)
=> new ComponentResult(Results.Challenge(properties, authenticationSchemes), ComponentResultType.Challenge);

/// <summary>
/// Creates a SignInHttpResult
/// </summary>
public static IComponentResult SignIn(
ClaimsPrincipal principal,
AuthenticationProperties properties = null,
string authenticationScheme = null)
=> new ComponentResult(Results.SignIn(principal, properties, authenticationScheme), ComponentResultType.SignIn);

/// <summary>
/// Creates a SignOutHttpResult
/// </summary>
public static IComponentResult SignOut(AuthenticationProperties properties = null, IList<string> authenticationSchemes = null)
=> new ComponentResult(Results.SignOut(properties, authenticationSchemes), ComponentResultType.SignOut);

/// <summary>
/// Creates a FileContentHttpResult
/// </summary>
public static IComponentResult File(
byte[] fileContents,
string contentType = null,
string fileDownloadName = null,
bool enableRangeProcessing = false,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue entityTag = null)
=> new ComponentResult(Results.File(fileContents, contentType, fileDownloadName, enableRangeProcessing, lastModified, entityTag), ComponentResultType.File);


/// <summary>
/// Creates a FileStreamHttpResult
/// </summary>
public static IComponentResult File(
Stream fileStream,
string contentType = null,
string fileDownloadName = null,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue entityTag = null,
bool enableRangeProcessing = false)
=> new ComponentResult(Results.File(fileStream, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), ComponentResultType.File);

/// <summary>
/// Returns either PhysicalFileHttpResult or VirtualFileHttpResult
/// </summary>
public static IComponentResult File(
string path,
string contentType = null,
string fileDownloadName = null,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue entityTag = null,
bool enableRangeProcessing = false)
=> new ComponentResult(Results.File(path, contentType, fileDownloadName, lastModified, entityTag, enableRangeProcessing), ComponentResultType.File);
}
33 changes: 23 additions & 10 deletions src/HydroComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public abstract class HydroComponent : TagHelper, IViewContextAware
private dynamic _viewBag;
private CookieStorage _cookieStorage;
private IPersistentState _persistentState;

private readonly ConcurrentDictionary<CacheKey, object> _requestCache = new();
private static readonly ConcurrentDictionary<CacheKey, object> PersistentCache = new();

Expand Down Expand Up @@ -166,7 +166,7 @@ private void ConfigurePolls()
/// </summary>
[HtmlAttributeNotBound]
public ViewDataDictionary ViewData => ViewContext.ViewData;

/// <summary>
/// View bag
/// </summary>
Expand Down Expand Up @@ -195,19 +195,19 @@ public dynamic ViewBag
/// </summary>
[HtmlAttributeNotBound]
public HttpContext HttpContext => ViewContext.HttpContext;

/// <summary>
/// Request
/// </summary>
[HtmlAttributeNotBound]
public HttpRequest Request => ViewContext.HttpContext.Request;

/// <summary>
/// Request
/// </summary>
[HtmlAttributeNotBound]
public HttpResponse Response => ViewContext.HttpContext.Response;

/// <summary>
/// RouteData
/// </summary>
Expand Down Expand Up @@ -236,7 +236,7 @@ public dynamic ViewBag
[HtmlAttributeNotBound]
public CookieStorage CookieStorage =>
_cookieStorage ??= new CookieStorage(HttpContext, _persistentState);

/// <summary>
/// Implementation of ViewComponent's InvokeAsync method
/// </summary>
Expand Down Expand Up @@ -299,12 +299,12 @@ private void SetupViewContext()
};

var view = compositeViewEngine.GetView(null, GetViewPath(), false).View;

if (view == null)
{
throw new InvalidOperationException($"The view '{GetViewPath()}' was not found.");
}

_writer = new StringWriter();
ViewContext = new ViewContext(ViewContext, view, viewDataDictionary, _writer);
}
Expand Down Expand Up @@ -479,7 +479,7 @@ public void Redirect(string url) =>
/// <param name="payload">Payload for the destination components</param>
public void Location(string url, object payload = null) =>
HttpContext.Response.HydroLocation(url, payload);

/// <summary>
/// Cache value
/// </summary>
Expand Down Expand Up @@ -933,7 +933,20 @@ private async Task TriggerMethod()

if (typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
await (Task)methodInfo.Invoke(this, orderedParameters)!;
if (methodInfo.ReturnType == typeof(Task<IComponentResult>))
{
var result = await (Task<IComponentResult>)methodInfo.Invoke(this, orderedParameters)!;
await result.ExecuteAsync(HttpContext, this);
}
else
{
await (Task)methodInfo.Invoke(this, orderedParameters)!;
}
}
else if (typeof(IComponentResult).IsAssignableFrom(methodInfo.ReturnType))
{
var result = (IComponentResult)methodInfo.Invoke(this, orderedParameters)!;
await result.ExecuteAsync(HttpContext, this);
}
else
{
Expand Down
12 changes: 6 additions & 6 deletions src/HydroComponentsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Hydro.Configuration;
using Hydro.Utils;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -61,6 +55,12 @@ string method
}

var htmlContent = await TagHelperRenderer.RenderTagHelper(componentType, httpContext);

if (httpContext.Response.Headers.ContainsKey(HydroConsts.ResponseHeaders.SkipOutput))
{
return HydroEmptyResult.Instance;
}

var content = await GetHtml(htmlContent);
return Results.Content(content, MediaTypeNames.Text.Html);
});
Expand Down
Loading

0 comments on commit 2d5cd44

Please sign in to comment.