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

Implemented extensible way to deal with results of commands. This is … #916

Merged
merged 22 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c9e9560
Implemented extensible way to deal with results of commands. This is …
Dec 8, 2020
9b164ee
Sample testing the exception filter that changes CommandResult and Cu…
Dec 9, 2020
35744dd
Moved command result assignment to ViewModelSerializer
quigamdev Dec 9, 2020
f13d1e4
CustomData are returned on postbacks too
tomasherceg Dec 9, 2020
b921c1c
Command added to the test samples
tomasherceg Dec 9, 2020
977696a
- Added UI test
quigamdev Dec 29, 2020
2de91de
Update src/DotVVM.Framework/Resources/Scripts/postback/postbackCore.ts
quigamdev Dec 30, 2020
a0f37a5
Update src/DotVVM.Framework/Resources/Scripts/global-declarations.ts
quigamdev Dec 30, 2020
6ba6afa
CommandResult removed from DotvvmContext.
Mylan719 Jan 9, 2021
05ca21a
Fixing uncompillable tests
Mylan719 Jan 13, 2021
101a60e
Custom data renamed to custom response properties. Custom response pr…
Mylan719 Jan 13, 2021
5784e12
Renaming and refactoring the custom presponse properties sample.
Mylan719 Jan 13, 2021
48de474
Changed custom response property test to work with renamed UI handle.
Mylan719 Jan 13, 2021
fdcb359
More tests for viewmodel response building and static command respons…
Jan 13, 2021
11194c3
Now the error message should be gramaticaly and factually correct.
Mylan719 Jan 14, 2021
3ab69eb
Merge
Mylan719 Jan 14, 2021
02fdf6a
Now Custom properties complain when property is added after the conte…
Mylan719 Jan 14, 2021
d8eb1dc
Merge branch 'main' into ferature/command-response-customisation
Mylan719 Jan 14, 2021
6435964
Correcting faulty implementation of TestDotvvmRequestContext.
Mylan719 Jan 14, 2021
42b98ea
Removed setters for custom response properties manager. Nobody should…
Mylan719 Jan 21, 2021
b8a7bad
Merge branch 'main' into ferature/command-response-customisation
quigamdev Jan 21, 2021
ae0d2bd
Fixed CommandResultTests.SimpleExceptionFilterTest
quigamdev Jan 21, 2021
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
2 changes: 1 addition & 1 deletion src/DotVVM.Framework.PerfTests/SerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void Serialize()
{
viewModel.Nested.Array[0].MyProperty = Guid.NewGuid().ToString(); // modify a bit

serializer.BuildViewModel(request);
serializer.BuildViewModel(request, null);
LastViewModel = serializer.SerializeViewModel(request);
if (allowDiffs) request.ReceivedViewModelJson = (JObject)request.ViewModelJson["viewModel"];
}
Expand Down
738 changes: 433 additions & 305 deletions src/DotVVM.Framework.Tests.Owin/DefaultViewModelSerializerTests.cs

Large diffs are not rendered by default.

23 changes: 11 additions & 12 deletions src/DotVVM.Framework/Hosting/DotvvmPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context)
// run the init phase in the page
DotvvmControlCollection.InvokePageLifeCycleEventRecursive(page, LifeCycleEventType.Init);
await requestTracer.TraceEvent(RequestTracingConstants.InitCompleted, context);
object? commandResult = null;

object? commandResult = null;
if (!isPostBack)
{
// perform standard get
Expand Down Expand Up @@ -268,8 +268,7 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context)
}
await requestTracer.TraceEvent(RequestTracingConstants.ViewModelSerialized, context);

ViewModelSerializer.BuildViewModel(context);
if (commandResult != null) context.ViewModelJson!["commandResult"] = JToken.FromObject(commandResult);
ViewModelSerializer.BuildViewModel(context, commandResult);

if (!context.IsInPartialRenderingMode)
{
Expand Down Expand Up @@ -373,7 +372,8 @@ public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context)

var result = await ExecuteCommand(actionInfo, context, filters);

await OutputRenderer.WriteStaticCommandResponse(context,
await OutputRenderer.WriteStaticCommandResponse(
context,
ViewModelSerializer.BuildStaticCommandResponse(context, result));
}
finally
Expand All @@ -393,17 +393,22 @@ await OutputRenderer.WriteStaticCommandResponse(context,
}

object? result = null;
Task? resultTask = null;

try
{
Task? resultTask = null;

result = action.Action();

resultTask = result as Task;
if (resultTask != null)
{
await resultTask;
}

if (resultTask != null)
{
result = TaskUtils.GetResult(resultTask);
}
}
catch (Exception ex)
{
Expand All @@ -428,12 +433,6 @@ await OutputRenderer.WriteStaticCommandResponse(context,
{
throw new Exception("Unhandled exception occurred in the command!", context.CommandException);
}

if (resultTask != null)
{
return TaskUtils.GetResult(resultTask);
}

return result;
}

Expand Down
12 changes: 12 additions & 0 deletions src/DotVVM.Framework/Hosting/DotvvmRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public class DotvvmRequestContext : IDotvvmRequestContext
public IViewModelSerializer ViewModelSerializer => Services.GetRequiredService<IViewModelSerializer>();

private IServiceProvider? _services;

public IServiceProvider Services
{
get => _services ?? (_services = Configuration.ServiceProvider ?? throw new NotSupportedException());
Expand All @@ -118,6 +119,8 @@ public IServiceProvider Services

public IHttpContext HttpContext { get; set; }

private readonly Dictionary<string, object> customResponseProperties = new Dictionary<string, object>();
public IReadOnlyDictionary<string, object> CustomResponseProperties => customResponseProperties;
public DotvvmRequestContext(
IHttpContext httpContext,
DotvvmConfiguration configuration,
Expand All @@ -136,5 +139,14 @@ public static DotvvmRequestContext GetCurrent(IHttpContext httpContext)
return httpContext.GetItem<DotvvmRequestContext>(HostingConstants.DotvvmRequestContextOwinKey)
.NotNull();
}

public void AddCustomResponseProperty(string key, object value)
{
if(customResponseProperties.ContainsKey(key))
{
throw new InvalidOperationException($"Custom property {key} already exists.");
}
customResponseProperties[key] = value;
}
}
}
2 changes: 2 additions & 0 deletions src/DotVVM.Framework/Hosting/IDotvvmRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,7 @@ public interface IDotvvmRequestContext
string? ResultIdFragment { get; set; }

IServiceProvider Services { get; }
IReadOnlyDictionary<string, object> CustomResponseProperties { get; }
void AddCustomResponseProperty(string key, object value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ interface DotvvmPostbackHandlerCollection {

type DotvvmStaticCommandResponse = {
result: any;
customData: { [key: string]: any };
} | {
action: "redirect";
url: string;
Expand Down
13 changes: 7 additions & 6 deletions src/DotVVM.Framework/Resources/Scripts/postback/postback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function postBack(
serverResponseObject,
wasInterrupted,
commandResult: null,
response: (err.reason as any).response,
response: (err.reason as any)?.response,
Copy link
Member

Choose a reason for hiding this comment

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

The reason should not be null, why is the ?. needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

When an exception is thrown the reason si null. That's why null propagation is needed.

Copy link
Member

Choose a reason for hiding this comment

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

Exception on the server? That would be type: "serverError". Exception on the client should also contain the reason when it's of type DotvvmPostbackError. DotvvmPostbackError's type definition does not allow null, so could either update it or (preferably IMHO) fix the bug that's causing null in there?

Copy link
Contributor

Choose a reason for hiding this comment

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

The null in reason was caused by an exception in dotvvm.events.<>.subscirbe((e)=>{ "some error here" }). In this case, you need to expect that it can be null. It is non-dotvvm code that will result in this exception and dotvvm has to work with it.
The type definition works only for typescript. But not everyone has to be using it.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, I'd better add a null check to the constructor. If you terribly need to be null-tollerant, I'd suggest changing the type signature so we can at least be consistent. Even with your additional null checks, the handler is going to crash in isInterruptingErrorReason

Copy link
Member

Choose a reason for hiding this comment

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

@quigamdev I think we should add try/catch blocks around errors in event handlers and wrap the error with reason: event. But I think we can do it in a separate PR.

error: err
}
events.afterPostback.trigger(eventArgs);
Expand All @@ -78,7 +78,7 @@ export async function postBack(
const errorEventArgs: DotvvmErrorEventArgs = {
...options,
serverResponseObject,
response: (err.reason as any).response,
response: (err.reason as any)?.response,
error: err,
handled: false
}
Expand All @@ -89,7 +89,7 @@ export async function postBack(
return {
...options,
serverResponseObject,
response: (err.reason as any).response,
response: (err.reason as any)?.response,
error: err
};
}
Expand Down Expand Up @@ -153,7 +153,7 @@ export async function applyPostbackHandlers(
const errorEventArgs: DotvvmErrorEventArgs = {
...options,
serverResponseObject,
response: (err.reason as any).response,
response: (err.reason as any)?.response,
error: err,
handled: false
}
Expand All @@ -165,7 +165,7 @@ export async function applyPostbackHandlers(
return {
...options,
serverResponseObject,
response: (err.reason as any).response,
response: (err.reason as any)?.response,
error: err
};
}
Expand Down Expand Up @@ -282,6 +282,7 @@ function shouldTriggerErrorEvent(err: DotvvmPostbackError) {
return err.reason.type == "network" || err.reason.type == "serverError";
}
function extractServerResponseObject(err: DotvvmPostbackError) {
if (!err.reason) return null;
if (err.reason.type == "commit" && err.reason.args) {
return err.reason.args.serverResponseObject;
}
Expand All @@ -291,4 +292,4 @@ function extractServerResponseObject(err: DotvvmPostbackError) {
return err.reason.responseObject;
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,5 @@ type PostbackResponse =
commandResult: any
action: string
resultIdFragment?: string
customData?: { [key: string]: any }
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export async function staticCommandPostback(sender: HTMLElement, command: string
methodId: command,
methodArgs: args,
error: err,
result: (err.reason as any).responseObject,
response: (err.reason as any).response
result: (err.reason as any)?.responseObject,
response: (err.reason as any)?.response
})

throw err;
Expand Down
12 changes: 12 additions & 0 deletions src/DotVVM.Framework/Testing/TestDotvvmRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,22 @@ public class TestDotvvmRequestContext : IDotvvmRequestContext
public DotvvmView View { get; set; }

private IServiceProvider _services;

public IServiceProvider Services
{
get => _services ?? Configuration?.ServiceProvider ?? throw new NotSupportedException();
set => _services = value;
}

private readonly Dictionary<string, object> customResponseProperties = new Dictionary<string, object>();
public IReadOnlyDictionary<string, object> CustomResponseProperties => customResponseProperties;
public void AddCustomResponseProperty(string key, object value)
{
if (customResponseProperties.ContainsKey(key))
{
throw new InvalidOperationException($"Custom property {key} already exists.");
}
customResponseProperties[key] = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public string SerializeViewModel(IDotvvmRequestContext context)
/// <summary>
/// Builds the view model for the client.
/// </summary>
public void BuildViewModel(IDotvvmRequestContext context)
public void BuildViewModel(IDotvvmRequestContext context, object commandResult)
{
// serialize the ViewModel
var serializer = CreateJsonSerializer();
Expand Down Expand Up @@ -121,6 +121,9 @@ public void BuildViewModel(IDotvvmRequestContext context)
}
// TODO: do not send on postbacks
if (validationRules?.Count > 0) result["validationRules"] = validationRules;

if (commandResult != null) result["commandResult"] = WriteCommandData(commandResult, serializer, "result");
AddCustomPropertiesIfAny(context, serializer, result);

context.ViewModelJson = result;
}
Expand All @@ -140,18 +143,33 @@ public string BuildStaticCommandResponse(IDotvvmRequestContext context, object r
UsedSerializationMaps = new HashSet<ViewModelSerializationMap>()
};
serializer.Converters.Add(viewModelConverter);
var writer = new JTokenWriter();
var response = new JObject();
response["result"] = WriteCommandData(result, serializer, "result");
AddCustomPropertiesIfAny(context, serializer, response);
return response.ToString(JsonFormatting);
}

private static void AddCustomPropertiesIfAny(IDotvvmRequestContext context, JsonSerializer serializer, JObject response)
{
if (context.CustomResponseProperties != null && context.CustomResponseProperties.Count > 0)
{
response["customProperties"] = WriteCommandData(context.CustomResponseProperties, serializer, "custom properties");
}
}

private static JToken WriteCommandData(object data, JsonSerializer serializer, string description)
{
var writer = new JTokenWriter();
try
{
serializer.Serialize(writer, result);
serializer.Serialize(writer, data);
}
catch (Exception ex)
{
throw new Exception($"Could not serialize viewModel of type { context.ViewModel.GetType().Name }. Serialization failed at property { writer.Path }. {GeneralViewModelRecommendations}", ex);
throw new Exception($"Could not serialize static command {description} of type '{ data.GetType().FullName}'. Serialization failed at property { writer.Path }. {GeneralViewModelRecommendations}", ex);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's imho missing an article. "Could not serialize a static command {description} object of type ..." sounds a bit better to me.

Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: It will say "static command", even commands. We could just say "Could not serialize result"/"Could not serialize custom data".

}
response["result"] = writer.Token;
return response.ToString(JsonFormatting);

return writer.Token;
}

protected virtual JsonSerializer CreateJsonSerializer() => DefaultSerializerSettingsProvider.Instance.Settings.Apply(JsonSerializer.Create);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace DotVVM.Framework.ViewModel.Serialization
{
public interface IViewModelSerializer
{
void BuildViewModel(IDotvvmRequestContext context);
void BuildViewModel(IDotvvmRequestContext context, object commandResult);

string BuildStaticCommandResponse(IDotvvmRequestContext context, object result);
string BuildStaticCommandResponse(IDotvvmRequestContext context, object commandResult);

string SerializeViewModel(IDotvvmRequestContext context);

Expand Down
1 change: 1 addition & 0 deletions src/DotVVM.Samples.Common/DotVVM.Samples.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<None Remove="Views\FeatureSamples\Resources\LocationFallback.dothtml" />
<None Remove="Views\FeatureSamples\Resources\RequiredOnPostback.dothtml" />
<None Remove="Views\FeatureSamples\Serialization\DeserializationVirtualElements.dothtml" />
<None Remove="Views\FeatureSamples\CustomResponseProperties\SimpleExceptionFilter.dothtml" />
<None Remove="Views\FeatureSamples\StaticCommand\StaticCommand_ArrayAssigment.dothtml" />
<None Remove="Views\FeatureSamples\StaticCommand\StaticCommand_LoadComplexDataFromService.dothtml" />
<None Remove="Views\FeatureSamples\StaticCommand\StaticCommand_NullAssignment.dothtml" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.Runtime.Filters;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomResponseProperties
{
public class PageErrorModel
{
public string Message { get; set; }
}
public class ClientExceptionFilterAttribute : ExceptionFilterAttribute
{
protected override Task OnCommandExceptionAsync(IDotvvmRequestContext context, ActionInfo actionInfo, Exception exception)
{
if (exception is UIException clientError)
{
context.AddCustomResponseProperty("validation-errors",new PageErrorModel {
Message = clientError.Message
});
context.AddCustomResponseProperty("Message", "Hello there");

context.IsCommandExceptionHandled = true;
}
return Task.FromResult(0);
}
}
public class UIException : Exception
{
public UIException(string message) : base(message)
{
}
}
public class SimpleExceptionFilterViewModel : DotvvmViewModelBase
{
[AllowStaticCommand]
[ClientExceptionFilter]
public static void StaticCommand()
{
throw new UIException("Problem!");
}

[ClientExceptionFilter]
public void Command()
{
throw new UIException("Problem!");
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomResponseProperties.SimpleExceptionFilterViewModel, DotVVM.Samples.Common
@import model = DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomResponseProperties.SimpleExceptionFilterViewModel

<html>
<head>
</head>
<body>
<div>
<dot:Button Click="{staticCommand: model.StaticCommand()}" data-ui="staticCommand" Text="Static Command Test" />
<dot:Button Click="{command: Command()}" Text="Command Test" data-ui="command" />
<dot:Button onclick="javascript: clearTexts()" Text="Clear" data-ui="clear"></dot:Button>
</div>
<p>
<span data-ui="customProperties"></span><br />
</p>
<dot:InlineScript>
function clearTexts(){
var customProperties = document.querySelector('[data-ui="customProperties"]');
customProperties.innerText = "";
}
dotvvm.events.staticCommandMethodInvoked.subscribe(e => {
var customPropertiesInput = document.querySelector('[data-ui="customProperties"]');
customPropertiesInput.innerText = e.serverResponseObject.customProperties.Message;
});

dotvvm.events.postbackResponseReceived.subscribe(e => {
var customPropertiesInput = document.querySelector('[data-ui="customProperties"]');
customPropertiesInput.innerText = e.serverResponseObject.customProperties.Message;
});
</dot:InlineScript>
</body>
</html>

Loading