Skip to content

Commit

Permalink
Use request body to transfer data. Introduce client actions.
Browse files Browse the repository at this point in the history
  • Loading branch information
kjeske committed Apr 1, 2024
1 parent e2f430f commit 4595edb
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 122 deletions.
19 changes: 13 additions & 6 deletions src/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static (string Name, IDictionary<string, object> Parameters)? GetNameAndP
{
return null;
}

var name = methodCall.Method.Name;
var paramInfos = methodCall.Method.GetParameters();
var arguments = methodCall.Arguments;
Expand All @@ -34,7 +34,7 @@ public static (string Name, IDictionary<string, object> Parameters)? GetNameAndP
return (name, parameters);
}

private static object EvaluateExpressionValue(Expression expression)
internal static object EvaluateExpressionValue(Expression expression)
{
switch (expression)
{
Expand All @@ -53,17 +53,24 @@ private static object EvaluateExpressionValue(Expression expression)
&& callExpression.Arguments.Any()
&& callExpression.Arguments[0] is ConstantExpression constantExpression:

return EncodeJsExpression(constantExpression.Value);
var value = ReplaceJsQuotes(constantExpression.Value?.ToString() ?? string.Empty);

return EncodeJsExpression(value);

default:
throw new NotSupportedException("Unsupported expression type: " + expression.GetType().Name);
}
}

private static string EncodeJsExpression(object expression) =>
$"{JsIndicationStart}{expression}{JsIndicationEnd}";

internal static string ReplaceJsQuotes(string value) =>
value
.Replace("\"", "&quot;")
.Replace("'", "&apos;");

internal static string DecodeJsExpressionsInJson(string json) =>
json.Replace("\"" + JsIndicationStart, "")
.Replace(JsIndicationEnd + "\"", "");

private static string EncodeJsExpression(object expression) =>
$"{JsIndicationStart}{expression}{JsIndicationEnd}";
}
15 changes: 15 additions & 0 deletions src/HydroClientActions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Hydro;

/// <summary>
/// Actions that are evaluated on the client side
/// </summary>
public class HydroClientActions
{
/// <summary>
/// Invoke JavaScript expression on client side
/// </summary>
/// <param name="jsExpression">JavaScript expression</param>
public void Invoke(string jsExpression)
{
}
}
50 changes: 24 additions & 26 deletions src/HydroComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public abstract class HydroComponent : ViewComponent
/// Determines if the whole model was accessed already
/// </summary>
public bool IsModelTouched { get; set; }

/// <summary>
/// Determines if the current execution is related to the component mounting
/// </summary>
Expand All @@ -81,12 +81,12 @@ public HydroComponent()
private void ConfigurePolls()
{
var componentType = GetType();

if (Polls.ContainsKey(componentType))
{
return;
}

var methods = componentType
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.DeclaringType != typeof(HydroComponent))
Expand All @@ -99,12 +99,12 @@ private void ConfigurePolls()
{
throw new InvalidOperationException("Poll can be defined only on custom actions");
}

if (methods.Any(p => p.ParametersCount != 0))
{
throw new InvalidOperationException("Poll can be defined only on actions without parameters");
}

if (methods.Any(p => p.Attribute.Interval <= 0))
{
throw new InvalidOperationException("Polling's interval is invalid");
Expand All @@ -113,7 +113,7 @@ private void ConfigurePolls()
var polls = methods
.Select(p => new HydroPoll(p.Method.Name, TimeSpan.FromMilliseconds(p.Attribute.Interval)))
.ToList();

Polls.TryAdd(componentType, polls);
}

Expand Down Expand Up @@ -212,6 +212,11 @@ public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Paren
});
}

/// <summary>
/// Provides actions that can be executed on client side
/// </summary>
public HydroClientActions Client { get; } = new();

/// <summary>
/// Triggered once the component is mounted
/// </summary>
Expand All @@ -236,7 +241,7 @@ public virtual Task RenderAsync()
Render();
return Task.CompletedTask;
}

/// <summary>
/// Triggered before each render
/// </summary>
Expand Down Expand Up @@ -335,7 +340,7 @@ private async Task<string> RenderOnlineRootComponent(IPersistentState persistent
? await GenerateComponentHtml(componentId, persistentState)
: string.Empty;
}

private async Task<string> RenderOnlineNestedComponent(IPersistentState persistentState)
{
var componentId = GenerateComponentId(Key);
Expand Down Expand Up @@ -391,11 +396,11 @@ private async Task<string> GenerateComponentHtml(string componentId, IPersistent
{
var previousParentComponentId = HttpContext.Items[HydroConsts.Component.ParentComponentId];
HttpContext.Items[HydroConsts.Component.ParentComponentId] = componentId;

var componentHtmlDocument = await GetComponentHtml();

HttpContext.Items[HydroConsts.Component.ParentComponentId] = previousParentComponentId;

var root = componentHtmlDocument.DocumentNode;

if (root.ChildNodes.Count(n => n.NodeType == HtmlNodeType.Element) != 1)
Expand All @@ -418,7 +423,7 @@ private async Task<string> GenerateComponentHtml(string componentId, IPersistent
rootElement.AppendChild(GetPollScript(componentHtmlDocument, polls[i], i));
}
}

rootElement.AppendChild(GetModelScript(componentHtmlDocument, componentId, persistentState));

foreach (var subscription in _subscriptions)
Expand Down Expand Up @@ -450,7 +455,7 @@ private async Task BindModel(IFormCollection formCollection)
{
var setter = PropertyInjector.GetPropertySetter(this, pair.Key, pair.Value);
var propertyPath = PropertyPath.ExtractPropertyPath(pair.Key);

if (setter != null)
{
var value = _options.ValueMappersDictionary.TryGetValue(setter.Value.Value.GetType(), out var mapper)
Expand Down Expand Up @@ -526,7 +531,7 @@ private HtmlNode GetEventSubscriptionScript(HtmlDocument document, HydroEventSub
scriptNode.SetAttributeValue("x-on-hydro-event", JsonConvert.SerializeObject(eventData));
return scriptNode;
}

private HtmlNode GetPollScript(HtmlDocument document, HydroPoll poll, int index)
{
var scriptNode = document.CreateElement("script");
Expand Down Expand Up @@ -566,7 +571,7 @@ private async Task TriggerMethod()
var methodInfos = GetType()
.GetMethods(BindingFlags.Public | BindingFlags.Instance);

var requestParameters = GetParameters();
var requestParameters = (IDictionary<string, object>)HttpContext.Items[HydroConsts.ContextItems.Parameters];

var methodInfo = methodInfos.FirstOrDefault(m =>
string.Equals(m.Name, methodValue, StringComparison.OrdinalIgnoreCase)
Expand Down Expand Up @@ -608,7 +613,7 @@ private async Task TriggerMethod()
{
return null;
}

var sourceType = requestParameter.GetType();

if (p.ParameterType == sourceType)
Expand Down Expand Up @@ -636,11 +641,6 @@ private async Task TriggerMethod()
}
}

private IDictionary<string, object> GetParameters() =>
HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.Parameters, out var parameters)
? JsonConvert.DeserializeObject<Dictionary<string, object>>(parameters, JsonSerializerSettings)
: new Dictionary<string, object>();

/// <summary>
/// Get the payload transferred from previous page's component
/// </summary>
Expand Down Expand Up @@ -669,9 +669,7 @@ private async Task TriggerEvent()
var methodInfo = subscription.Action.Method;
var parameters = methodInfo.GetParameters();
var parameterType = parameters.First().ParameterType;
var model = HttpContext.Items.TryGetValue(HydroConsts.ContextItems.EventData, out var eventModel)
? JsonConvert.DeserializeObject((string)eventModel!, parameterType)
: null;
var model = JsonConvert.DeserializeObject((string)HttpContext.Items[HydroConsts.ContextItems.EventData], parameterType);

var operationId = HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
? incomingOperationId.First()
Expand Down Expand Up @@ -753,7 +751,7 @@ private async Task<HtmlDocument> GetComponentHtml()
{
using var stream = new MemoryStream();
await using var writer = new StreamWriter(stream);

var previousWriter = ViewComponentContext.ViewContext.Writer;
ViewComponentContext.ViewContext.Writer = writer;
ViewComponentContext.ViewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;
Expand Down Expand Up @@ -871,7 +869,7 @@ public bool Validate()
private bool ValidateTouched()
{
ModelState.Clear();

var context = new ValidationContext(this, serviceProvider: null, items: null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(this, context, validationResults, true);
Expand Down
70 changes: 28 additions & 42 deletions src/HydroComponentsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,63 +59,49 @@ string method
var htmlContent = await viewComponentHelper.InvokeAsync(componentType);

var content = await GetHtml(htmlContent);

return Results.Content(content, MediaTypeNames.Text.Html);
});
}

private static async Task ExecuteRequestOperations(HttpContext context, string method)
{
var requestModel = context.Request.Headers[HydroConsts.RequestHeaders.Model];
var renderedComponentIDs = context.Request.Headers[HydroConsts.RequestHeaders.RenderedComponentIds];
context.Items.Add(HydroConsts.ContextItems.RenderedComponentIds, JsonConvert.DeserializeObject<string[]>(renderedComponentIDs));

if (!string.IsNullOrWhiteSpace(requestModel))
if (!context.Request.HasFormContentType)
{
context.Items.Add(HydroConsts.ContextItems.BaseModel, requestModel.ToString());
throw new InvalidOperationException("Hydro form doesn't contain form which is required");
}

if (!string.IsNullOrEmpty(method))
{
if (method == HydroConsts.Component.EventMethodName)
{
var eventName = context.Request.Headers[HydroConsts.RequestHeaders.EventName];
var hydroData = await context.Request.ReadFormAsync();

if (!string.IsNullOrWhiteSpace(eventName))
{
context.Items.Add(HydroConsts.ContextItems.EventName, eventName.ToString());
}
}
else if (!string.IsNullOrWhiteSpace(method))
{
context.Items.Add(HydroConsts.ContextItems.MethodName, method);
}
else
{
context.Items.Add(HydroConsts.ContextItems.IsBind, true);
}
var formValues = hydroData
.Where(f => !f.Key.StartsWith("__hydro"))
.ToDictionary(f => f.Key, f => f.Value);

var model = hydroData["__hydro_model"].First();
var type = hydroData["__hydro_type"].First();
var parameters = JsonConvert.DeserializeObject<Dictionary<string, object>>(hydroData["__hydro_parameters"].FirstOrDefault("{}"));
var eventData = JsonConvert.DeserializeObject<HydroEventPayload>(hydroData["__hydro_event"].FirstOrDefault(string.Empty));
var componentIds = JsonConvert.DeserializeObject<string[]>(hydroData["__hydro_componentIds"].FirstOrDefault("[]"));
var form = new FormCollection(formValues);

context.Items.Add(HydroConsts.ContextItems.RenderedComponentIds, componentIds);
context.Items.Add(HydroConsts.ContextItems.BaseModel, model);
context.Items.Add(HydroConsts.ContextItems.Parameters, parameters);

if (eventData != null)
{
context.Items.Add(HydroConsts.ContextItems.EventName, eventData.Name);
context.Items.Add(HydroConsts.ContextItems.EventData, eventData.Data);
}

if (context.Request.HasFormContentType)
if (!string.IsNullOrWhiteSpace(method) && type != "event")
{
var form = await context.Request.ReadFormAsync();
context.Items.Add(HydroConsts.ContextItems.RequestForm, form);
context.Items.Add(HydroConsts.ContextItems.MethodName, method);
}
else

if (form.Any())
{
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();

if (!string.IsNullOrWhiteSpace(body))
{
if (method == "event")
{
context.Items.Add(HydroConsts.ContextItems.EventData, body);
}
else
{
context.Items.Add(HydroConsts.ContextItems.RequestData, body);
}
}
context.Items.Add(HydroConsts.ContextItems.RequestForm, form);
}
}

Expand Down
8 changes: 1 addition & 7 deletions src/HydroConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@ internal static class HydroConsts
{
public static class RequestHeaders
{
public const string Model = "hydro-model";
public const string EventName = "hydro-event-name";
public const string ClientEventName = "Hydro-Client-Event-Name";
public const string Boosted = "Hydro-Boosted";
public const string Hydro = "Hydro-Request";
public const string Parameters = "Hydro-Parameters";
public const string OperationId = "Hydro-Operation-Id";
public const string Payload = "Hydro-Payload";
public const string RenderedComponentIds = "hydro-all-ids";
}

public static class ResponseHeaders
Expand All @@ -28,10 +23,9 @@ public static class ContextItems
public const string RenderedComponentIds = "hydro-all-ids";
public const string EventName = "hydro-event";
public const string MethodName = "hydro-method";
public const string IsBind = "hydro-bind";
public const string BaseModel = "hydro-base-model";
public const string RequestForm = "hydro-request-form";
public const string RequestData = "hydro-request-model";
public const string Parameters = "hydro-parameters";
public const string EventData = "hydro-event-model";
public const string IsRootRendered = "hydro-root-rendered";
}
Expand Down
9 changes: 9 additions & 0 deletions src/HydroEventPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Hydro;

internal class HydroEventPayload
{
public string Name { get; set; }
public object Data { get; set; }
public Scope Scope { get; set; }
public string OperationId { get; set; }
}
Loading

0 comments on commit 4595edb

Please sign in to comment.