0

I have the following entity models:

public class AssetLabel
{
    public string QRCode { get; set; }
    public string asset { get; set; }
    public virtual IEnumerable<Conversation> Conversations { get; set; }
}

public class Conversation
{
    public int ID { get; set; }
    public virtual AssetLabel AssetLabel{ get; set; }
    public string FinderName { get; set; }
    public string FinderMobile { get; set; } 
    public string FinderEmail  { get; set; }
    public  ConversationStatus Status{ get; set; }

    public IEnumerable<ConversationMessage> Messages { get; set; }
}

public class ConversationMessage
{
    public int ID { get; set; }
    public DateTime MessageDateTime { get; set; }
    public bool IsFinderMessage { get; set; }
    public virtual Conversation Conversation { get; set; }
}

public enum ConversationStatus { open, closed };

public class FinderViewModel : Conversation
{/*used in Controllers->Found*/


}

My MVC application will prompt for a QRCode on a POST request. I then validate this code exists in the database AssetLabel and some other server-side logic is satisfied. I then need to request the user contact details to create a new Conversation record. Currently I have a GET to a controller action which returns the first form to capture the Code. If this is valid then I create a new FinderViewModel, populate the AssetLabel with the object for the QRCode and return a view to consume the vm and show the fields for the Name, Mobile and Email. My problem is that although the AssetLabel is being passed to the view as part of the FinderViewModel and I can display fields from the AssetLabel; graphed object the AssetLabel does not get passed back in the POST. I know I could modify the FinderViewModel so that it takes the Conversation as one property and set up the QRCode as a separate property that could be a hidden field in the form and then re-find the the AssetLabel as part of the processing of the second form but this feels like a lot of work seeing as I have already validated it once to get to the point of creating the second form (this is why I am moving away from PHP MVC frameworks).

The first question is HOW?, The second question is am I approaching this design pattern in the wrong way. Is there a more .NETty way to persist the data through multiple forms? At this point in my learning I don't really want to store the information in a cookie or use ajax.

For reference the rest of the code for the 1st form POST, 2nd view and 2nd form POST are shown below (simplified to eliminate irrelevant logic).

public class FoundController : Controller
{
    private ApplicationDbContext db = new ApplicationDbContext();

    // GET: Found
    public ActionResult Index()
    {
        AssetLabel lbl = new AssetLabel();
        return View(lbl);
    }

    [HttpPost]
    public ActionResult Index(string QRCode)
    {
        if (QRCode=="")
        {
            return Content("no value entered");
        }
        else
        {
            /*check to see if code is in database*/
            AssetLabel lbl = db.AssetLables.FirstOrDefault(q =>q.QRCode==QRCode);
            if (lbl != null)
            {
                var vm = new FinderViewModel();
                vm.AssetLabel = lbl;
                vm.Status = ConversationStatus.open;

                return View("FinderDetails", vm);
            }
            else
            {/*Label ID is not in the database*/
                return Content("Label Not Found");
            }
        }
    }

    [HttpPost]
    public ActionResult ProcessFinder(FinderViewModel vm)
    {
        /*
        THIS IS WHERE I AM STUCK! - vm.AssetLabel == NULL even though it
        was passed to the view with a fully populated object
        */
        return Content(vm.AssetLabel.QRCode.ToString());
        //return Content("Finder Details posted!");
    }

FinderView.cshtml

@model GMSB.ViewModels.FinderViewModel

@{
     ViewBag.Title = "TEST FINDER";
}

<h2>FinderDetails</h2>

@using (Html.BeginForm("ProcessFinder","Found",FormMethod.Post))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
    <h4>Finder Details</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.ID)
    @Html.HiddenFor(model => model.AssetLabel)

    <div class="form-group">
        @Html.LabelFor(model => model.FinderName, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.FinderName, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.FinderName, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.FinderMobile, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.FinderMobile, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.FinderMobile, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.FinderEmail, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.FinderEmail, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.FinderEmail, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
        </div>
    </div>
</div>

}

Rendered HTML snippet for AssetLabel

<input id="AssetLabel" name="AssetLabel" type="hidden"       
value="System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3" 
/>
Aaron Reese
  • 131
  • 11

2 Answers2

2

You cannot use @Html.HiddenFor() to generate a hidden output for a complex object. Internally the method use .ToString() to generate the value (in you case the output is System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3 which cannot be bound back to a complex object)

You could generate a form control for each property of the AssetLabel - but that would be unrealistic in your case because AssetLabel contains a property with is a collection of Conversation which in turn contains a collection of ConversationMessage so you would need nested for loops to generate an input for each property of Conversation and ConversationMessage.

But sending a whole lot of extra data to the client and then sending it all back again unchanged degrades performance, exposes unnecessary details about your data and data structure to malicious users, and malicious users could change the data).

The FinderViewModel should just contain a property for QRCode (or the ID property of AssetLabel) and in the view

@Html.HiddenFor(m => m.QRCode)

Then in the POST method, if you need the AssetLabel, get it again from the repository just as your doing it in the GET method (although its unclear why you need to AssetLabel in the POST method).

As a side note, a view model should only contain properties that are needed in the view, and not contain properties which are data models (in in your case inherit from a data model) when editing data. Refer What is ViewModel in MVC?. Based on your view, it should contain 4 properties FinderName, FinderMobile, FinderEmail and QRCode (and int? ID if you want to use it for editing existing objects).

Community
  • 1
  • 1
0

Thanks Stephen. The QRCode is the PK on AssetLabel and the FK in Conversation so it needs to be tracked through the workflow. I was trying to keep the viewModel generic so that is can be used for other forms rather than tightly coupling it to this specific form and I was trying to pass the AssetLabel around as I have already done a significant amount of validation on it's state which I didn't want to repeat. I worked out what I need to do - If you use @Html.Hidden(model => model.AssetLabel.QRCode) then the form field name becomes AssetLabel_QRCode and is automatically mapped to the correct place in the POST viewmodel. To promote code reuse and avoid any rework later I have created this logic in a display template with the fields defined as hidden and then @Html.Partial() using the overload method that allows me to define the model extension to the form names

@Html.Partial
(
    "./Templates/Assetlabel_hidden", 
    (GMSB.Models.AssetLabel)(Model.AssetLabel), 
    new ViewDataDictionary()
    {
        TemplateInfo = new TemplateInfo()
        {
            HtmlFieldPrefix = "AssetLabel"
        }
    }
)

But you are absolutely right, this exposes additional fields and my application structure. I think I will redraft the viewModel to only expose the necessary fields and move the AssetLabel validation to a separate private function that can be called from both the initial POST and the subsequent post. This does mean extra code in the controller as the flat vm fields need to be manually mappped to the complex object graph.

Aaron Reese
  • 131
  • 11