I have a view with a form that can be edited. The view is rather complicated. The form is for a contract. The contract name can be edited at the top. A contract has many ads, and the details for each of those ads can be edited. The length of the form depends on how many ads a contract has. I have a for loop in the view that displays all the form fields for each ad.
Furthermore, there are some fields in the model that are used to determine some of the ad details, but themselves shouldn't be saved to the database (yet they must appear as, say, dropdowns in the view).
Also, there are some model fields that don't need to appear in the view but are needed in the POST controller method in order to save the database properly (I'm saving it manually), so they must go through the view somehow and be passed back to the controller. Here's my model:
public class ModContract
{
public int contract_id; // pk; needed
public string contract_name { get; set; } // save
public List<ModAds> ads { get; set; }
}
public class ModAds
{
public int contr_ad_id; // pk; needed
public string name { get; set; } // save
public string print_product_id { get; set; } // used for product_name
public string product_name { get; set; } // appear
public string print_ad_option_id { get; set; } // save
public string adv_product { get; set; } // appear
public List<string> editions { get; set; } // save
public double freq_disc { get; set; } // save
public double other_dis_dol { get; set; } // save
public double? other_dis_per { get; set; } // save
public string non_cash_note { get; set; } // save
public double non_cash_cons { get; set; } // save
}
The fields that have a comment of "save" after them should be saved to the database. The ones with "appear" must be in the view but are not needed in the POST controller method. The ones with "needed" are needed in the POST controller method but not the view.
By the way, these model classes don't map one-to-one with database tables, they're made from several different tables.
As far as I know I don't see how I can get model binding to work with this. The model that's passed back to the controller has a null value for the ads field of ModContract, for example. I would like to know how I can manually pass what I want from the view back to the controller without relying on model binding.
I know I might use @Html.HiddenFor for the ids needed in the POST method but not the view, but for some reason when I tried it this with the contract_id and _name, it didn't work.
<li data-role="fieldcontain">
<label>Contract Name</label>
@Html.HiddenFor(m => m.contract_id)
@Html.EditorFor(m => m.contract_name)
</li>
The contract_id that was returned in the POST method was 0.
Let me know if my question sounds stupid because this is impossible or because I'm overcomplicating things, I'm pretty new to all this.
Thanks in advance!
EDIT
Here's the view. I can't get it to indent right but hopefully it's readable.
@model oulookmediaweb.Models.ModContract
@{
ViewBag.Title = "ModifyContract";
}
@{
var num = 1;
}
<h2>Modify Contract</h2>
@using (Html.BeginForm("ModifyContract", "Contract", FormMethod.Post, htmlAttributes: new { data_ajax = "false" }))
{
@Html.ValidationSummary();
<ul data-role="listview" data-inset="true">
<li data-role="fieldcontain">
<label>Contract Name</label>
@Html.HiddenFor(m => m.contract_id)
@Html.EditorFor(m => m.contract_name)
</li>
</ul>
<div>
@foreach (var ad in Model.ads)
{
<div id="@num">
<ul data-role="listview" data-inset="true">
<li data-role="list-divider">@ad.name</li>
<li data-role="fieldcontain">
<label><strong>Product Name</strong></label>
<div>@ad.product_name</div>
<div id="@num-drdn0" hidden="true">@Html.DropDownListFor(m => ad.print_product_id, ViewData["products_list"] as SelectList)</div>
<input id="@num-editbutton" type="button" onclick="edit(@num)" value="Edit" />
</li>
<li data-role="fieldcontain">
<label><strong>Advertising Product</strong></label>
<div>@ad.adv_product</div>
<div id="@num-drdn1" class="hid">
<select></select>
</div>
</li>
<li data-role="fieldcontain">
<label><strong>Editions to run in:</strong></label>
@foreach (var ed in ad.editions)
{
<div>@ed</div>
}
<div id="@num-drdn2" class="hid">
<select multiple="multiple" data-native-menu="false"></select>
</div>
</li>
<li data-role="fieldcontain">
<label><strong>Frequency Discount (%)</strong></label>
<div>@Html.EditorFor(m => ad.freq_disc)</div>
</li>
<li data-role="fieldcontain">
<label><strong>Other Discount ($)</strong></label>
<div>@Html.EditorFor(m => ad.other_dis_dol)</div>
</li>
<li data-role="fieldcontain">
<label><strong>Other Discount (%)</strong></label>
<div>@Html.EditorFor(m => ad.other_dis_per)</div>
</li>
<li data-role="fieldcontain">
<label><strong>Non Cash Note</strong></label>
<div>@Html.EditorFor(m => ad.non_cash_note)</div>
</li>
<li data-role="fieldcontain">
<label><strong>Non Cash Consideration</strong></label>
<div>@Html.EditorFor(m => ad.non_cash_cons)</div>
</li>
</ul>
@{num++;}
</div>
}
</div>
<ul data-role="listview" data-inset="true">
<li data-role="fieldcontain">
<input type="submit" data-theme="e" value="Submit" />
</li>
</ul>
}
<script type="text/javascript">
var nu;
window.onload = function () {
// hide adv prods and editions select for now
$(".hid select").closest('.ui-select').hide();
}
// called when the edit button for product is clicked
function edit(num) {
nu = num;
$("#" + nu + "-drdn0").show(); // show dropdown
$("#" + nu + "-editbutton").closest('.ui-btn').hide(); // hide edit button; '.ui-btn'? WTF?
// remove current product selection div
// remove adv product selection div
// remove editions div
$("#" + nu + "-drdn0 select").change(prodChange); // on select change
$("#" + nu + "-drdn0 select").trigger("change"); // trigger for default; happens twice; WHY?
}
// called when a magazine is selected
function prodChange() {
// ajax
var url = '@Url.Action("GetAdvProdList", "Contract")' + '?prod_id=' + this.value;
$.getJSON(url, null, function (list) {
// for adv list dropdown
$("#" + nu + "-drdn1 select").empty(); // remove old stuff
$("#" + nu + "-drdn1 select").closest('.ui-select').show(); // show dropdown
var arr = list.advlist; // get the array from object
$.each(arr, function (ind, val) {
// add each item in list as an option of select
$("#" + nu + "-drdn1 select").append('<option value=' + val.Value + '>' + val.Text + '</option>');
});
$("#" + nu + "-drdn1 select").selectmenu('refresh', true); // refresh menu
// for ed list
$("#" + nu + "-drdn2 select").empty(); // remove old stuff
$("#" + nu + "-drdn2 select").closest('.ui-select').show(); // show list
var lis = list.edlist; // get the array from object
$.each(lis, function (ind, val) {
// add each item to list
$("#" + nu + "-drdn2 select").append('<option value=' + val.Value + '>' + val.Text + '</option>');
});
$("#" + nu + "-drdn2 select").selectmenu('refresh', true); // refresh menu
});
}
You have quite a few issues there and understandably if you have a complex form. So let me try and summarize your problem to give the best possible solution.
there are some fields in the model...(that) themselves shouldn't be saved to the database (yet they must appear as, say, dropdowns in the view)
You put these fields in the model and treat them as read-only . I'll show later how.
there are some model fields that don't need to appear in the view but are needed in the POST controller method in order to save the database properly (I'm saving it manually)
You don't need to do that. You are passing unnecessary data around and you are introducing over posting. In addition, you are not allowing your users to edit them and yet you pass it to the client, receive it, and save it back to the database.
It even exposes a security hole in your system. For example, you are passing product_name
to the view, don't want it to be edited, but then save it to another table. Someone can easily hack that value and you could end up with a product_name
that you never expected.
The fields that have a comment of "save" after them should be saved to the database.
Accept these as inputs on your form. Code will be shown later.
The ones with "appear" must be in the view but are not needed in the POST controller method.
As I've mentioned these should be read-only fields on your viewmodel, code shown later.
The ones with "needed" are needed in the POST controller method but not the view.
You don't need to pass this around nor query it right away. Get it later from your database when saving-time comes. Pseudo-code shown later.
At this point I assume you understand what I mentioned above by answering some points of your question. Here are the relevant codes to those explanation.
public class ContractViewModel {
// start:read-only fields.
// These are the fields that will be used as read-only
public string product_name { get; set; }
public string adv_product { get; set; }
public IEnumerable<SelectListItem> PrintAdOptions { get; set; }
// end:read-only fields.
public InputModel Contract {get;set;}
}
// you use this class to capture "all your inputs"
public class InputModel {
public int contract_id {get;set;}
public string contract_name { get; set; }
public IEnumerable<InputSubModel> ads {get;set;}
}
public class InputSubModel {
public int contr_ad_id {get;set;}
public string print_ad_option_id {get;set;} // is this really a string?
public string name {get;set;}
}
You will create a "master" viewmodel called ContractViewModel
and use it on your view (of course you can have any name you want). It contains the fields to show information and an "input" field to capture all your inputs. It is a flatten representation of all your entities. The "input" field, called InputModel
is another flatten representation of all your entities. The inputs from that viewmodel will be dispersed and persisted properly.
// method for creating a contract
public ActionResult Create() {
// implement the ff methods, whatever method you're using ORM, plain ADO, etc.
var product = getEntityFromYourDb();
// you may eagerly load this (ORM) or query it separately, it's up to you
// AdOption is a simple entity with structure: Id, Name
IEnumerable<AdOption> adOptions = getAdOptionsFromDb();
// map entity to model
// I am guessing here that "product" has the following fields.
// Just replace this accordingly based on you entities
var model = new ContractViewModel{
product_name = product.Name,
adv_product = product.AdvProduct,
PrintAdOptions = aOptions.Select(x=>
new SelectListItem{ Value = x.Id, Text = x.Name })
Contract = getEntitiesFromDbAndMapToModel();
};
// at this point you have everything you need
// (1) a field that you can just show (and not save)
// (2) a list of fields that you can use on a drop-down
// (3) input fields, the ONLY ones that you will POST
return View(model);
}
The example given here also solve this problem:
As far as I know I don't see how I can get model binding to work with this. The model that's passed back to the controller has a null value for the ads field of ModContract
Never use a foreach
on a collection as the razor engine cannot properly write it in a way that you can save it back. The reason for this is that the elements written have the same name and they cannot be properly binded (or bound, I think is the right word in English - binded in programming :). Always use a for-loop
, see it in the following code.
@model ContractViewModel
<h2>Modify Contract</h2>
<p>@Model.product_name</p>
<p>@Model.adv_product</p>
@Html.HiddenFor(m=>m.Contract.contract_id) // your reference Id for saving
@Html.TextBoxFor(m=>m.Contract.contract_name)
@for(var i=0;i<Model.Contract.ads.Count();i++){
@Html.HiddenFor(m=>m.Contract.ads[i].contr_ad_id) // your reference Id for saving
@Html.TextBoxFor(m=>m.Contract.ads[i].name)
@Html.DropdownListFor(m=>m.Contract.ads[i].print_ad_option_id, m.PrintAdOptions)
}
In the example given below you will notice we used the Bind
attribute. That tells the controller to only bind and get our "input" model and that is the InputModel
. There is no need to post back all the other values that you won't need for saving or for querying, etc. So take note that if you need a field, say an id
(eg product id) that you will later on use to query a product from the product table, because you need to save its name to the ModAds
table then include that in the InputModel
. So that you will have, InputModel.ProductId
.
[HttpPost]
public ActionResult Create([Bind(Prefix = "Contract")]InputModel model) {
// map back the values from the flatten model into your entities
// it could be a single entity or a complex one
// but I'm sure you can easily determine how to do it
// as an example:
var modContract = getContractFromDb(model.contract_id);
modContract.contract_name = model.contract_name;
// do the rest of the mapping here
}
That is the closest example that I can give you based on your model and what you are trying to do. If you follow along carefully with that example, I'm sure it will solve your problem.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.