2
votes

I'm trying to get simple ASP.NET CORE web app going and I'm running into issues with allowing the user to access a file on my server via a html link. I have a model called "TestModel", a view called "TestView" and a controller called "AppController". My view allows the user to input some text in two separate fields as well as select two different files from their hard drive, it binds everything to my model and performs a POST when the user clicks a button. I have verified that the model is correctly being passed back to my controller. My controller correctly saves the files to a folder in my server directory, uses a separate service to manipulate the files, and returns the model back to the view, i.e. when I use the debugger to inspect "return View(model)" in the Testview action the model being passed back has all it's properties populated.

The issue is that I want two links on the view to point to the files on the server so that the user can click the link and receive a prompt to download the files, but I can't seem to get it going. I am using the @Html.ActionLink() capability in razor to point to a "Download descriptions" action and pass the model to it, thinking that it would be passing the current view's model, but the model it passes has all fields as null. Any insight into what I am doing incorrectly?

These are the relevant actions in my AppController:

        [HttpPost("~/App/TestView")]
        public IActionResult TestView(TestModel Model)
        {
            if (ModelState.IsValid)
            {
                Console.WriteLine("Something was sent back from the TestView page");
                Model.OnPostAsync();
                var x = _descLogicService.Convert(Model);
            }

            return View(Model);
        }

        public IActionResult DownloadDescriptions(TestModel model)
        {
            var cd = new ContentDispositionHeaderValue("attachment")
            {
                FileNameStar = model.DescriptionFile.FileName
            };

            Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());

            byte[] bytes = System.IO.File.ReadAllBytes(model.MeasurementExportFile);

            using (FileStream fs = new FileStream(model.MeasurementExportFile, FileMode.Open, FileAccess.Read))
            {
                fs.Read(bytes, 0, System.Convert.ToInt32(fs.Length));
                fs.Close();
            }

            return File(bytes, "text/csv");

        }

Here is my View Model:

 public class TestModel
    {
        [BindProperty]
        [Required]
        [MinLength(5)]
        public string Name { get; set; }
        [BindProperty]
        [Required]
        [MaxLength(10,ErrorMessage ="Input string is too long.")]
        public string MyInput { get; set; }

        [BindProperty]
        [Required]
        public IFormFile DescriptionFile { get; set; }


        [BindProperty]
        [Required]
        public IFormFile MeasurementFile { get; set; }
        public string DescriptionDirectory { get; set; } 
        public string DescriptionExportFile { get; set; } 
        public string MeasurementDirectory { get; set; } 
        public string MeasurementExportFile { get; set; }

        public async Task OnPostAsync()
        {
            var token = DateTime.Now.Ticks.ToString();

            var directory =  Directory.CreateDirectory(@"uploads\"+ token + @"\");
            DescriptionDirectory = directory.CreateSubdirectory("Descriptions").FullName + @"\";
            DescriptionExportFile = directory.CreateSubdirectory(@"Descriptions\Exports\").FullName + DescriptionFile.FileName;
            MeasurementDirectory = directory.CreateSubdirectory("Measurements").FullName + @"\";
            MeasurementExportFile = directory.CreateSubdirectory(@"Measurements\Exports\").FullName + MeasurementFile.FileName;

            var file = Path.Combine(DescriptionDirectory, DescriptionFile.FileName);
            using (var fileStream = new FileStream(file, FileMode.Create))
            {
                await DescriptionFile.CopyToAsync(fileStream).ConfigureAwait(true);
            }

             file = Path.Combine(MeasurementDirectory, MeasurementFile.FileName);
            using (var fileStream = new FileStream(file, FileMode.Create))
            {
                await MeasurementFile.CopyToAsync(fileStream).ConfigureAwait(true);
            }
        }
    }

And here is the View:

@**I need to add the namespace of C# models I'm creating *@
@using FirstASPNETCOREProject.ViewModels
@*I need to identify the model which 'fits' this page, that is the properties of the model can be
    bound to entities on the view page, using "asp-for"*@
@model TestModel
@{
    ViewData["Title"] = "Page for File Uploads";
}
@section Scripts{
}

<div asp-validation-summary="ModelOnly" style="color:white"></div>
<form method="post" enctype="multipart/form-data">
    <label>Enter a Description File Name</label>
    <input asp-for="Name" type="text" />
    <span asp-validation-for="Name"></span>
    <br>
    <label>Select a Description File</label>
    <input asp-for="DescriptionFile" type="file" />
    <span asp-validation-for="DescriptionFile"></span>
    <br>
    <label>Enter the Measurement File Name</label>
    <input asp-for="MyInput" type="text">
    <span asp-validation-for="MyInput"></span>
    <br>
    <label>Select a Measurement File</label>
    <input asp-for="MeasurementFile" type="file">
    <span asp-validation-for="MeasurementFile"></span>
    <br>
    <input type="submit" value="Send Message" />
</form>


@Html.ActionLink("Description File", "DownloadDescriptions", "App")
@Html.ActionLink("Measurement File", "DownloadMeasurements", "App")
1
Welcome to StackOverflow. You can't use ActionLink to pass a model. You would need to use a second and third form containing the values you want to POST to the Download actions. That, or you create a GET request and supply the values you need as route parameters. The latter could be done with the ActionLink. - Dennis VW
@Dennis1679 that is NOT true... you can pass a model using ActionLinks... you just need to add an anonymous type to the ActionLink call and set the parameter to [FromQuery] in the controller's action - Jonathan Alfaro
@JonathanAlfaro what you are describing isn't passing a model the way I meant 'passing'. What I was trying to say was, that you cannot pass a model through ActionLink as if you would pass it via a form/post. You can however - as you describe - trick it into creating a RouteValueDictionary from the model, because ActionLink would take an object, and that in turn will be translated into a query string. Which makes me wonder, would that even work for complex models containing lists? - Dennis VW
@Dennis1679 once again you are wrong.... It is perfectly possible to bind complex objects using Query String parameters.... As a matter of fact I just posted an example on how to do it. - Jonathan Alfaro
Just FYI, this combination works for me: In the view: @Html.ActionLink("Measurement File", "DownloadMeasurements", "App", Model). In the Controller: public IActionResult DownloadMeasurements([FromQuery]TestModel model){ }. Model in the view is a TestModel object. - V.P.

1 Answers

2
votes

ActionLinks are just anchor tags.

So they use GET. So to pass back your model using get you would need to use Query String parameters for example adding "?MyInpu=some cool input" at the end of the url.

You can bind almost ANY complex object like this including Lists and Arrays.

For more information on Model Binding

The file itself you wont be able to pass it like that. For that you will need to POST the form with a submit button or FormData using javascript.

You can also add anchors that call javascript functions that use AJAX to post back all you want to the DownloadDescriptions action in your controller.

Here is an example on how to pass a model using an action link:

 @Html.ActionLink("Test Action", "TestAction", "Home", new { myInput = "Some Cool Input" })

In my case the previous ActionLink produces an anchor tag with href set to:

http://localhost:64941/Home/TestAction?myInput=Some Cool Input

Notice how I used an anonymous type to pass the model using the same names of the properties of my model in this case MyInput but in camelized version myInput.

You can compose any model like that.

This is my action in my controller:

    public IActionResult TestAction([FromQuery]TestModel input) 
    {
        return View(input);
    }

Notice how I used [FromQuery] for the TestModel parameter to indicate that I expect the ASP.NET Core model binder to use the Query String parameters to populate my model.

This is my model class:

public class TestModel
{
    public string MyInput { get; set; }
}

This is the result during debugging:

enter image description here

Notice how during debugging I am able to see the populated value.

NOTES:

If your model changes at the client side. You will need to update the Query String parameters in the anchor tag using javascript... for that reason is a good idea to add name to the anchor tag.

Also this might answer your question but might NOT be the best approach to what you are trying to do.