简体   繁体   中英

How do I manually pass data from view to controller?

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.

The Breakdown

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.

The Proposed Solution

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.

Building The Model

// 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);
}

Consuming the Model on your View

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)
}

Finally, Saving Just What You Need

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
}

Conclusion

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM