Skip to content

Commit 5bd28ec

Browse files
authored
Merge pull request #22 from hydrostack/19-better-support-for-file-upload-via-binding
Support file upload via input binding
2 parents 7e0ccfb + f261a4d commit 5bd28ec

File tree

5 files changed

+194
-36
lines changed

5 files changed

+194
-36
lines changed

docs/content/features/binding.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,88 @@ public override void Bind(PropertyPath property, object value)
6060
// your logic
6161
}
6262
}
63-
```
63+
```
64+
65+
## File upload
66+
67+
You can use `file` inputs to enable file upload. Example:
68+
69+
```razor
70+
<!-- AddAttachment.cshtml -->
71+
72+
@model AddAttachment
73+
74+
<div>
75+
<input
76+
asp-for="DocumentFile"
77+
type="file"
78+
hydro-bind />
79+
</div>
80+
```
81+
82+
```c#
83+
// AddAttachment.cshtml.cs
84+
85+
public class AddAttachment : HydroComponent
86+
{
87+
[Transient]
88+
public IFormFile DocumentFile { get; set; }
89+
90+
[Required]
91+
public string DocumentId { get; set; }
92+
93+
public async Task Save()
94+
{
95+
if (!Validate())
96+
{
97+
return;
98+
}
99+
100+
var tempFilePath = GetTempFileLocation(DocumentId);
101+
102+
// Move your file at tempFilePath to the final storage
103+
// and save that information in your domain
104+
}
105+
106+
public override async Task BindAsync(PropertyPath property, object value)
107+
{
108+
if (property.Name == nameof(DocumentFile))
109+
{
110+
// assign the temp file name to the DocumentId
111+
DocumentId = await GetStoredTempFileId((IFormFile)value);
112+
}
113+
}
114+
115+
private static async Task<string> GetStoredTempFileId(IFormFile file)
116+
{
117+
if (file == null)
118+
{
119+
return null;
120+
}
121+
122+
var tempFileName = Guid.NewGuid().ToString("N");
123+
var tempFilePath = GetTempFileLocation(tempFileName);
124+
125+
await using var readStream = file.OpenReadStream();
126+
await using var writeStream = File.OpenWrite(tempFilePath);
127+
await readStream.CopyToAsync(writeStream);
128+
129+
return tempFileName;
130+
}
131+
132+
private static string GetTempFileLocation(string fileName) =>
133+
Path.Combine(Path.GetTempPath(), fileName);
134+
}
135+
```
136+
137+
`DocumentFile` property represents the file that is sent by the user. We need to put `[Transient]` attribute on it, to make sure it's not
138+
serialized, kept on the page, and sent back to the server each time - it would be a lot of data to transfer in case of large files.
139+
140+
The place where we interact with the uploaded file is the `BindAsync` method. We store the file in a temporary storage
141+
which we can use later when submitting the form.
142+
143+
> NOTE: Make sure the temporary storage is cleared periodically.
144+
145+
## Styling
146+
147+
`.hydro-request` CSS class is toggled on the elements that are currently in the binding process

src/HydroComponent.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,11 @@ private async Task BindModel(IFormCollection formCollection)
458458

459459
if (setter != null)
460460
{
461-
var value = _options.ValueMappersDictionary.TryGetValue(setter.Value.Value.GetType(), out var mapper)
462-
? await mapper.Map(setter.Value.Value)
463-
: setter.Value.Value;
461+
var value = setter.Value.Value == null
462+
? null
463+
: _options.ValueMappersDictionary.TryGetValue(setter.Value.Value.GetType(), out var mapper)
464+
? await mapper.Map(setter.Value.Value)
465+
: setter.Value.Value;
464466

465467
setter.Value.Setter(value);
466468
await BindAsync(propertyPath, value);
@@ -470,6 +472,22 @@ private async Task BindModel(IFormCollection formCollection)
470472
await BindAsync(propertyPath, null);
471473
}
472474
}
475+
476+
foreach (var file in formCollection.Files)
477+
{
478+
var setter = PropertyInjector.GetPropertySetter(this, file.Name, file);
479+
var propertyPath = PropertyPath.ExtractPropertyPath(file.Name);
480+
481+
if (setter != null)
482+
{
483+
setter.Value.Setter(file);
484+
await BindAsync(propertyPath, file);
485+
}
486+
else
487+
{
488+
await BindAsync(propertyPath, null);
489+
}
490+
}
473491
}
474492

475493
/// <summary>
@@ -620,14 +638,13 @@ private async Task TriggerMethod()
620638
{
621639
return requestParameter;
622640
}
623-
else if (p.ParameterType.IsEnum)
641+
642+
if (p.ParameterType.IsEnum)
624643
{
625644
return Enum.ToObject(p.ParameterType, requestParameter);
626645
}
627-
else
628-
{
629-
return TypeDescriptor.GetConverter(p.ParameterType).ConvertFrom(requestParameter);
630-
}
646+
647+
return TypeDescriptor.GetConverter(p.ParameterType).ConvertFrom(requestParameter);
631648
})
632649
.ToArray();
633650

src/HydroComponentsExtensions.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313
using Microsoft.AspNetCore.Routing;
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Hydro.Configuration;
16+
using Hydro.Utils;
1617
using Microsoft.Extensions.Logging;
1718
using Newtonsoft.Json;
1819

1920
namespace Hydro;
2021

2122
internal static class HydroComponentsExtensions
2223
{
24+
private static readonly JsonSerializerSettings JsonSerializerSettings = new()
25+
{
26+
Converters = new JsonConverter[] { new Int32Converter() }.ToList()
27+
};
28+
2329
public static void MapHydroComponent(this IEndpointRouteBuilder app, Type componentType)
2430
{
2531
var componentName = componentType.Name;
@@ -79,10 +85,10 @@ private static async Task ExecuteRequestOperations(HttpContext context, string m
7985

8086
var model = hydroData["__hydro_model"].First();
8187
var type = hydroData["__hydro_type"].First();
82-
var parameters = JsonConvert.DeserializeObject<Dictionary<string, object>>(hydroData["__hydro_parameters"].FirstOrDefault("{}"));
88+
var parameters = JsonConvert.DeserializeObject<Dictionary<string, object>>(hydroData["__hydro_parameters"].FirstOrDefault("{}"), JsonSerializerSettings);
8389
var eventData = JsonConvert.DeserializeObject<HydroEventPayload>(hydroData["__hydro_event"].FirstOrDefault(string.Empty));
8490
var componentIds = JsonConvert.DeserializeObject<string[]>(hydroData["__hydro_componentIds"].FirstOrDefault("[]"));
85-
var form = new FormCollection(formValues);
91+
var form = new FormCollection(formValues, hydroData.Files);
8692

8793
context.Items.Add(HydroConsts.ContextItems.RenderedComponentIds, componentIds);
8894
context.Items.Add(HydroConsts.ContextItems.BaseModel, model);
@@ -99,7 +105,7 @@ private static async Task ExecuteRequestOperations(HttpContext context, string m
99105
context.Items.Add(HydroConsts.ContextItems.MethodName, method);
100106
}
101107

102-
if (form.Any())
108+
if (form.Any() || form.Files.Any())
103109
{
104110
context.Items.Add(HydroConsts.ContextItems.RequestForm, form);
105111
}

src/PropertyInjector.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.ComponentModel;
44
using System.Reflection;
5+
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.Mvc;
67
using Microsoft.Extensions.Primitives;
78
using Newtonsoft.Json;
@@ -89,7 +90,7 @@ public static void SetPropertyValue(object target, string propertyPath, object v
8990
propertyInfo.SetValue(currentObject, convertedValue);
9091
}
9192

92-
public static (object Value, Action<object> Setter)? GetPropertySetter(object target, string propertyPath, StringValues value)
93+
public static (object Value, Action<object> Setter)? GetPropertySetter(object target, string propertyPath, object value)
9394
{
9495
if (target == null)
9596
{
@@ -177,7 +178,7 @@ private static (int, string) GetIndexAndCleanedPropertyName(string propName)
177178
return (Convert.ToInt32(iteratorValue), cleanedPropName);
178179
}
179180

180-
private static (object, Action<object>)? SetValueOnObject(object obj, string propName, StringValues valueToSet)
181+
private static (object, Action<object>)? SetValueOnObject(object obj, string propName, object valueToSet)
181182
{
182183
if (obj == null)
183184
{
@@ -205,7 +206,7 @@ private static (object, Action<object>)? SetValueOnObject(object obj, string pro
205206
return (convertedValue, val => propertyInfo.SetValue(obj, val));
206207
}
207208

208-
private static (object, Action<object>)? SetIndexedValue(object obj, string propName, StringValues valueToSet)
209+
private static (object, Action<object>)? SetIndexedValue(object obj, string propName, object valueToSet)
209210
{
210211
var (index, cleanedPropName) = GetIndexAndCleanedPropertyName(propName);
211212
var propertyInfo = obj.GetType().GetProperty(cleanedPropName);
@@ -241,15 +242,26 @@ private static (object, Action<object>)? SetIndexedValue(object obj, string prop
241242
throw new InvalidOperationException($"Indexed access for property '{cleanedPropName}' is not supported.");
242243
}
243244

244-
private static object ConvertValue(StringValues valueToConvert, Type destinationType)
245+
private static object ConvertValue(object valueToConvert, Type destinationType)
245246
{
246-
var converter = TypeDescriptor.GetConverter(destinationType);
247+
if (valueToConvert is not StringValues stringValues)
248+
{
249+
return valueToConvert;
250+
}
251+
252+
if (typeof(IFormFile).IsAssignableFrom(destinationType) && StringValues.IsNullOrEmpty(stringValues))
253+
{
254+
return null;
255+
}
256+
257+
var converter = TypeDescriptor.GetConverter(destinationType!);
258+
247259
if (!converter.CanConvertFrom(typeof(string)))
248260
{
249261
throw new InvalidOperationException($"Cannot convert StringValues to '{destinationType}'.");
250262
}
251263

252-
if (!destinationType.IsArray || valueToConvert.Count <= 1)
264+
if (!destinationType.IsArray || stringValues is { Count: <= 1 })
253265
{
254266
try
255267
{
@@ -262,12 +274,12 @@ private static object ConvertValue(StringValues valueToConvert, Type destination
262274
}
263275

264276
var elementType = destinationType.GetElementType();
265-
var array = Array.CreateInstance(elementType, valueToConvert.Count);
266-
for (var i = 0; i < valueToConvert.Count; i++)
277+
var array = Array.CreateInstance(elementType, stringValues.Count);
278+
for (var i = 0; i < stringValues.Count; i++)
267279
{
268280
try
269281
{
270-
array.SetValue(converter.ConvertFromString(valueToConvert[i]), i);
282+
array.SetValue(converter.ConvertFromString(stringValues[i]), i);
271283
}
272284
catch
273285
{

0 commit comments

Comments
 (0)