NedaM
NedaM

Reputation: 133

How to Interact between a view component's form and a controller in ASP.NET Core?

I'm beginner in web designing with ASP.NET Core. I wrote a view component that has a form with some inputs related to a view model. One of these inputs is a file input (of the IFormFile datatype).

I want to submit this view model to an action of a controller (POST action), check the validity of model, return another view component if the model state is valid, and remain on this view component with this view model if model state is not valid.

This is my View Model: PricingViewModel.cs

public class PricingViewModel
{
    [Display(Name = "Select a file")]
    public IFormFile formFile { get; set; }

    [Display(Name = "ColumnCode")]
    [Required(ErrorMessage = "Enter {0} value, please")]
    public string colCode { get; set; }

    [Display(Name = "ColumnName")]
    [Required(ErrorMessage = "Enter {0} value, please")]         
    public string colName { get; set; }
}   

My View Component (controller): PricingComponent.cs

public class PricingComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {               
        return await Task.FromResult((IViewComponentResult)View("PricingView", pricing));
    }
}

My View Component (view): PricingView.cshtml

<form class="text-left" method="post" enctype="multipart/form-data">

   <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

   <div class="form-group text-left">
     <label asp-for="colCode" class="control-label"></label>
     <input asp-for="colCode" class="form-control" id="colCodeId"/>
     <span asp-validation-for="colCode" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="colName" class="control-label"></label>
     <input asp-for="colName" class="form-control" id="colNameId"/>
     <span asp-validation-for="colName" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="formFile " class="control-label"></label>
     <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile"/>
   </div>

   <div class="form-group mt-4">
     <input type="submit" asp-action="ShowPricing" asp-controller="Home" value="Show" id="ShowPricingBtn" />
   </div>
</form>

My Home Controller: HomeController.cs

[HttpPost]
public IActionResult ShowPricing(PricingViewModel pricing)
{
    if (ModelState.IsValid)
    {
        int temp;
        if (!int.TryParse(pricing.colCode, out temp))
        {
            ViewBag.isValid = 0;
            ModelState.AddModelError("colCode", "Invalid Data");
            return ViewComponent("PricingComponent", new { pricing = pricing }); // 1
        }
        else if (!int.TryParse(pricing.colName, out temp))
        {
            ViewBag.isValid = 0;
            ModelState.AddModelError("colName", "Invalid Data");
            return ViewComponent("PricingComponent", new { pricing = pricing }); //2
        }
        else
        {
            ViewBag.isValid = 1;   
            
            // do something ...

            return ViewComponent("ShowPricingExcelComponent"); //Call another view component
        }
    }
    else
    {
       ViewBag.isValid = 0;
       return ViewComponent("PricingComponent", new { pricing = pricing }); //3
    }
}

Plan A

The above approach is my primary plan.

Problem

If I use options of submit input tag (asp-action, asp-controller) like above, the view model sends correctly, but I don't know how to handle the validity of the model and remain on this view component. In the above code, when the ShowPricing action runs, if the model state is valid, the code works correctly, but when model is invalid (1,2,3), the PricingView doesn't show the validation summery, and just loads with current view model.

Plan B

I used AJAX to send the viewModel to the action and instead of showing the validation summary, I send an alert to the user with AJAX. I changed PricingView as following:

My View Component (view): PricingView.cshtml

<form class="text-left" method="post" enctype="multipart/form-data">

   <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

   <div class="form-group text-left">
     <label asp-for="colCode" class="control-label"></label>
     <input asp-for="colCode" class="form-control" id="colCodeId"/>
     <span asp-validation-for="colCode" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="colName" class="control-label"></label>
     <input asp-for="colName" class="form-control" id="colNameId"/>
     <span asp-validation-for="colName" class="text-danger"></span>
   </div>

   <div class="form-group text-left">
     <label asp-for="fromFile " class="control-label"></label>
     <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile"/>
   </div>

    <script>
      $(document).ready(function () {
        $('#ShowPricingBtn').click(function () {
                var _url = '@Url.Action("ShowPricing", "Home")';
                var input = $("#MyInputFile").get(0).files[0]; 

                $.ajax({
                    type: "POST",
                    url: _url,
                    data: {
                       formFile: input,
                       colCode: $("#colCode").val(),
                       colName: $("#colName").val(),
                    },
                    success: function (result) 
                    {
                       var IsValid = $('body').find('[name="IsValidPricing"]').val();
                       if (IsValid) 
                       {
                          $("#ShowExcelTable").html(result);
                       }
                       else {
                          alert("Invalid Data");
                       }
                    },
                });
           });
       });
    </script>
   <div class="form-group mt-4">
     <input type="submit" value="Show" id="ShowPricingBtn" />
   </div>
</form>

Problem

In this code:

  1. If the model state is not valid, the alert sends correctly, but
  2. If the model state is valid, the formFile input doesn't send correctly to action and it's null in view model.

I don't know whether I should go with the original or the alternate approach these problems. Do you know where I'm going wrong?

Upvotes: 4

Views: 3681

Answers (2)

Rena
Rena

Reputation: 36715

Not sure how do you call view components,here are the working demos:

For PlanA

1.Create ViewComponents/PricingComponent.cs and ViewComponents/ShowPricingExcelComponent.cs.

public class PricingComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {
        return await Task.FromResult((IViewComponentResult)View("PricingView", pricing));
    }
}    
public class ShowPricingExcelComponent : ViewComponent
{
    public async Task<IViewComponentResult> InvokeAsync(PricingViewModel pricing)
    {
        return await Task.FromResult((IViewComponentResult)View("ShowPricingExcel", pricing));
    }
}

2.Create Views/Shared/Components/PricingComponent/PricingView.cshtml.

@model PricingViewModel 
<form class="text-left" method="post" enctype="multipart/form-data">

    <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

    <div class="form-group text-left">
        <label asp-for="colCode" class="control-label"></label>
        <input asp-for="colCode" class="form-control" id="colCodeId" />
        <span asp-validation-for="colCode" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="colName" class="control-label"></label>
        <input asp-for="colName" class="form-control" id="colNameId" />
        <span asp-validation-for="colName" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="formFile " class="control-label"></label>
        <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile" />
    </div>

    <div class="form-group mt-4">
        <input type="submit" asp-action="ShowPricing" asp-controller="Home" value="Show" id="ShowPricingBtn" />
    </div>
</form>

3.Create Views/Shared/Components/ShowPricingExcelComponent/ShowPricingExcel.cshtml.

<h1>Excel....</h1>

Project Structure:

enter image description here

4.Views/Home/Index.cshtml:

@await Component.InvokeAsync("PricingComponent")

5.HomeController:

public class HomeController : Controller
{       
    public IActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public IActionResult ShowPricing(PricingViewModel pricing)
    {
        if (ModelState.IsValid)
        {
            int temp;
            if (!int.TryParse(pricing.colCode, out temp))
            {
                ViewBag.isValid = 0;
                ModelState.AddModelError("colCode", "Invalid Data");
                return View("Index", pricing);
            }
            if (!int.TryParse(pricing.colName, out temp))
            {
                ViewBag.isValid = 0;
                ModelState.AddModelError("colName", "Invalid Data");
                return View("Index", pricing);
            }
            else 
            {
                ViewBag.isValid = 1;

                // do something ...

                return ViewComponent("ShowPricingExcelComponent"); //Call another view component
            }              
        }
        else
        {
            ViewBag.isValid = 0;
            return View("Index", pricing); //3
        }
    }
}

Result:

enter image description here

For PlanB

1.Create ViewComponents/PricingComponent.cs and ViewComponents/ShowPricingExcelComponent.cs.

2.Create Views/Shared/Components/PricingComponent/PricingView.cshtml.

Firstly,it should be type="button" otherwise it will call twice to the backend.Secondly,what you did in ajax is not correct,more detailed explation you could refer to this answer.At last,you could not judge the modelstate by get the value of IsValidPricing value in your sucess function.Because the value you get is always be the data you first render the page,you cannot get the changed ViewBag value when ajax post back.

@model PricingViewModel
<form class="text-left" method="post" enctype="multipart/form-data">

    <input name="IsValidPricing" type="hidden" value="@ViewBag.isValid" />

    <div class="form-group text-left">
        <label asp-for="colCode" class="control-label"></label>
        <input asp-for="colCode" class="form-control" id="colCodeId" />
        <span asp-validation-for="colCode" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="colName" class="control-label"></label>
        <input asp-for="colName" class="form-control" id="colNameId" />
        <span asp-validation-for="colName" class="text-danger"></span>
    </div>

    <div class="form-group text-left">
        <label asp-for="formFile " class="control-label"></label>
        <input type="file" accept=".xlsx, .csv" asp-for="formFile" id="MyInputFile" />
    </div>

    <div class="form-group mt-4">
        @*it should be type="button"*@
        <input type="button" value="Show" id="ShowPricingBtn" />
    </div>
</form>

<script src="~/lib/jquery/dist/jquery.min.js"></script>

<script>
      $(document).ready(function () {
        $('#ShowPricingBtn').click(function () {
            var _url = '@Url.Action("ShowPricing", "Home")';
            var input = $("#MyInputFile").get(0).files[0];

            var fdata = new FormData();
            fdata.append("formFile", input);
            $("form input[type='text']").each(function (x, y) {
                fdata.append($(y).attr("name"), $(y).val());
            });
            $.ajax({
                type: "POST",
                url: _url,
                data: fdata,
                contentType: false,   
                processData: false,
                success: function (result)
                {
                    console.log(result);
                    if (result==false)
                    {
                        alert("Invalid Data");
                    }
                    else {
                        $("#ShowExcelTable").html(result);

                    }
                },
            });
        });
  });
</script>

3.Create Views/Shared/Components/ShowPricingExcelComponent/ShowPricingExcel.cshtml.

<h1>Excel....</h1>

4.Views/Home/Index.cshtml:

@await Component.InvokeAsync("PricingComponent")
<div id="ShowExcelTable"></div>

5.HomeController:

public class HomeController : Controller
{       
    public IActionResult Index()
    {
        return View();
    }
    [HttpPost]
    public IActionResult ShowPricing(PricingViewModel pricing)
    {
        if (ModelState.IsValid)
        {
            int temp;
            if (!int.TryParse(pricing.colCode, out temp)|| !int.TryParse(pricing.colName, out temp))
            {
                ViewBag.isValid = 0;
                return Json(false);
            }
            else
            {
                ViewBag.isValid = 1;

                // do something ...
                return ViewComponent("ShowPricingExcelComponent"); //Call another view component
            }
        }
        else
        {
            ViewBag.isValid = 0;
            return Json(false);
        }
    }
}

Result: enter image description here

Upvotes: 5

Just the benno
Just the benno

Reputation: 2601

I'm not able to reproduce your error. Your code, as presented, works as expected. A validation message is displayed.

To make it a working example, I've added a GET method first.

[HttpGet]
public IActionResult ShowPricing() => ViewComponent("PricingComponent", new { pricing = new PricingViewModel() });

Open the URL Home/ShowPricing

Fill out the form.

enter image description here

Send the form. And the validation message is displayed.

enter image description here

Upvotes: 1

Related Questions