From f5702a093910865267b20ca5fdba71c60fa5a9c0 Mon Sep 17 00:00:00 2001 From: Krzysztof Jeske Date: Tue, 1 Oct 2024 20:52:48 +0200 Subject: [PATCH] Support binding multiple files to one field --- docs/content/features/binding.md | 50 ++++++++++++++++++++++++ docs/content/features/errors-handling.md | 1 + src/HydroComponent.cs | 34 ++++++++++++---- src/PropertyInjector.cs | 22 ++++++++--- src/Scripts/hydro.js | 6 ++- 5 files changed, 97 insertions(+), 16 deletions(-) diff --git a/docs/content/features/binding.md b/docs/content/features/binding.md index 2ae3a4c..d8abbf4 100644 --- a/docs/content/features/binding.md +++ b/docs/content/features/binding.md @@ -142,6 +142,56 @@ which we can use later when submitting the form. > NOTE: Make sure the temporary storage is cleared periodically. +### Multiple files + +To support multiple files in one field, you can use the `multiple` attribute: + +```razor + + +@model AddAttachment + +
+ +
+``` + +Then your component code would change to: + + +```c# +// AddAttachment.cshtml.cs + +public class AddAttachment : HydroComponent +{ + [Transient] + public IFormFile[] DocumentFiles { get; set; } + + [Required] + public List DocumentIds { get; set; } + + public override async Task BindAsync(PropertyPath property, object value) + { + if (property.Name == nameof(DocumentFiles)) + { + DocumentIds = []; + var files = (IFormFile[])value; + + foreach (var file in files) + { + DocumentIds.Add(await GetStoredTempFileId(file)); + } + } + } + + // rest of the file same as in the previous example +} +``` + ## Styling `.hydro-request` CSS class is toggled on the elements that are currently in the binding process \ No newline at end of file diff --git a/docs/content/features/errors-handling.md b/docs/content/features/errors-handling.md index 559d6cf..0b6fdff 100644 --- a/docs/content/features/errors-handling.md +++ b/docs/content/features/errors-handling.md @@ -46,6 +46,7 @@ app.UseExceptionHandler(b => b.Run(async context => { if (!context.IsHydro()) { + context.Response.Redirect("/Error"); return; } diff --git a/src/HydroComponent.cs b/src/HydroComponent.cs index a144d7d..eb26b5b 100644 --- a/src/HydroComponent.cs +++ b/src/HydroComponent.cs @@ -64,7 +64,7 @@ public abstract class HydroComponent : ViewComponent /// Default identifier used to specify place of the page to replace when during location change /// public const string LocationTargetId = "hydro"; - + /// /// Provides list of already accessed component's properties /// @@ -569,15 +569,33 @@ private async Task BindModel(IFormCollection formCollection) } } - foreach (var file in formCollection.Files) + var fileGroups = formCollection.Files.GroupBy(m => m.Name); + + foreach (var fileGroup in fileGroups) { - var setter = PropertyInjector.GetPropertySetter(this, file.Name, file); - var propertyPath = PropertyPath.ExtractPropertyPath(file.Name); + var setter = PropertyInjector.GetPropertySetter(this, fileGroup.Key, null); + var propertyPath = PropertyPath.ExtractPropertyPath(fileGroup.Key); if (setter != null) { - setter.Value.Setter(file); - await BindAsync(propertyPath, file); + if (setter.Value.PropertyType.IsArray) + { + var formFiles = fileGroup.ToArray(); + setter.Value.Setter(formFiles); + await BindAsync(propertyPath, formFiles); + } + else if (typeof(IEnumerable).IsAssignableFrom(setter.Value.PropertyType)) + { + var formFiles = fileGroup.ToList(); + setter.Value.Setter(formFiles); + await BindAsync(propertyPath, formFiles); + } + else if (setter.Value.PropertyType == typeof(IFormFile)) + { + var formFile = fileGroup.First(); + setter.Value.Setter(formFile); + await BindAsync(propertyPath, formFile); + } } else { @@ -685,7 +703,7 @@ private void PopulateDispatchers() HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.Trigger, JsonConvert.SerializeObject(data, JsonSerializerSettings)); } - + private void PopulateClientScripts() { if (!_clientScripts.Any()) @@ -1084,4 +1102,4 @@ internal void AddClientScript(string script) => private static string Hash(string input) => $"W{Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(input)))}"; -} +} \ No newline at end of file diff --git a/src/PropertyInjector.cs b/src/PropertyInjector.cs index f90be6d..2f6f5cc 100644 --- a/src/PropertyInjector.cs +++ b/src/PropertyInjector.cs @@ -98,7 +98,7 @@ public static void SetPropertyValue(object target, string propertyPath, object v propertyInfo.SetValue(currentObject, convertedValue); } - public static (object Value, Action Setter)? GetPropertySetter(object target, string propertyPath, object value) + public static (object Value, Action Setter, Type PropertyType)? GetPropertySetter(object target, string propertyPath, object value) { if (target == null) { @@ -186,7 +186,7 @@ private static (int, string) GetIndexAndCleanedPropertyName(string propName) return (Convert.ToInt32(iteratorValue), cleanedPropName); } - private static (object, Action)? SetValueOnObject(object obj, string propName, object valueToSet) + private static (object, Action, Type)? SetValueOnObject(object obj, string propName, object valueToSet) { if (obj == null) { @@ -211,10 +211,10 @@ private static (object, Action)? SetValueOnObject(object obj, string pro var convertedValue = ConvertValue(valueToSet, propertyInfo.PropertyType); propertyInfo.SetValue(obj, convertedValue); - return (convertedValue, val => propertyInfo.SetValue(obj, val)); + return (convertedValue, val => propertyInfo.SetValue(obj, val), propertyInfo.PropertyType); } - private static (object, Action)? SetIndexedValue(object obj, string propName, object valueToSet) + private static (object, Action, Type)? SetIndexedValue(object obj, string propName, object valueToSet) { var (index, cleanedPropName) = GetIndexAndCleanedPropertyName(propName); var propertyInfo = obj.GetType().GetProperty(cleanedPropName); @@ -234,7 +234,7 @@ private static (object, Action)? SetIndexedValue(object obj, string prop throw new InvalidOperationException("Wrong type"); } - return (convertedValue, val => array.SetValue(val, index)); + return (convertedValue, val => array.SetValue(val, index), propertyInfo!.PropertyType); } if (typeof(IList).IsAssignableFrom(propertyInfo.PropertyType)) @@ -244,7 +244,7 @@ private static (object, Action)? SetIndexedValue(object obj, string prop throw new InvalidOperationException("Wrong type"); } - return (convertedValue, val => list[index] = val); + return (convertedValue, val => list[index] = val, propertyInfo!.PropertyType); } throw new InvalidOperationException($"Indexed access for property '{cleanedPropName}' is not supported."); @@ -261,6 +261,16 @@ private static object ConvertValue(object valueToConvert, Type destinationType) { return null; } + + if (typeof(IFormFile[]).IsAssignableFrom(destinationType) && StringValues.IsNullOrEmpty(stringValues)) + { + return Array.Empty(); + } + + if (typeof(IEnumerable).IsAssignableFrom(destinationType) && StringValues.IsNullOrEmpty(stringValues)) + { + return new List(); + } var converter = TypeDescriptor.GetConverter(destinationType!); diff --git a/src/Scripts/hydro.js b/src/Scripts/hydro.js index b92f889..24673c5 100644 --- a/src/Scripts/hydro.js +++ b/src/Scripts/hydro.js @@ -207,9 +207,11 @@ formData.set(propertyName, el.checked); } else if (el.tagName === "INPUT" && el.type === 'file') { if (el.files.length) { - formData.set(propertyName, el.files[0]); + Array.from(el.files).forEach((file) => { + formData.append(propertyName, file); + }); } else { - formData.set(propertyName, new Blob(), ''); + formData.set(propertyName, ''); } } else { formData.set(propertyName, el.value);