如何通过POST请求携带复杂对象模型

时间:2016-11-12 18:48:23

标签: asp.net-mvc http-post object-graph

我有以下实体模型:

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*/


}

我的MVC应用程序将在POST请求中提示QRCode。然后,我验证此代码存在于数据库AssetLabel中,并且满足其他一些服务器端逻辑。然后,我需要请求用户联系人详细信息以创建新的Conversation记录。 目前我对控制器操作有一个GET,它返回第一个表单以捕获代码。如果这是有效的,那么我创建一个新的FinderViewModel,使用AssetLabel的对象填充QRCode并返回一个视图以使用vm并显示{{1}的字段},NameMobile。 我的问题是虽然Email作为AssetLabel的一部分传递给视图,但我可以显示FinderViewModel的字段;在POST中,AssetLabel没有传回的图形对象。我知道我可以修改AssetLabel,以便将FinderViewModel作为一个属性并将Conversation设置为单独的属性,该属性可以是表单中的隐藏字段,然后重新找到QRCode作为第二种形式处理的一部分,但这感觉就像很多工作一样,因为我已经验证了它一次到达创建第二种形式的点(这就是为什么我要移动远离PHP MVC框架)。

第一个问题是如何?第二个问题是我是否以错误的方式接近这种设计模式。是否有更多.NETty方法可以通过多种形式保存数据?在我学习的这一点上,我并不想将信息存储在cookie中或使用ajax。

作为参考,第一种形式POST,第二种形式和第二种形式POST的其余代码如下所示(简化为消除不相关的逻辑)。

AssetLabel

FinderView.cshtml

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!");
    }

}

为AssetLabel

呈现HTML代码段
@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>

2 个答案:

答案 0 :(得分:1)

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?。根据您的观点,它应包含4个属性FinderNameFinderMobileFinderEmailQRCode(以及int? ID,如果您想用它来编辑现有对象)。

答案 1 :(得分:0)

谢谢斯蒂芬。 QRCode是AssetLabel上的PK和Conversation中的FK,因此需要通过工作流程进行跟踪。我试图保持viewModel通用,以便可以用于其他表单,而不是将它紧密地耦合到这个特定的表单,我试图通过AssetLabel,因为我已经对它的状态进行了大量的验证我不想重复。我找到了我需要做的事情 - 如果您使用@ Html.Hidden(model =&gt; model.AssetLabel.QRCode),那么表单字段名称将变为AssetLabel_QRCode并自动映射到POST视图模型中的正确位置。为了促进代码重用并避免以后的任何返工,我在显示模板中创建了这个逻辑,其中字段定义为隐藏,然后使用重载方法@ Html.Partial(),允许我定义表单名称的模型扩展

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

但是你绝对正确,这暴露了额外的领域和我的应用程序结构。我想我将重新设计viewModel以仅显示必要的字段并将AssetLabel验证移动到可以从初始POST和后续帖子调用的单独的私有函数。这确实意味着控制器中存在额外的代码,因为需要手动将平坦的vm字段映射到复杂的对象图。