2

A record is uniquely identified by its Kind and RecordId.

I have a set of records that I want to display as a dropdown, and on submitting I'd like to get SelectedKind and SelectedRecordId for further processing.

The problem is, I want these values to be processed separately, but there should be only one dropdown.
What is the best practice for doing this?

My suggestions:

Option 1

Populate the dropdown with strings like "Kind_RecordId" and set up a JavaScript change handler which will parse the string and set hidden field values.

However I don't quite like the idea of having string operation logic in different places (in controller when populating the dropdown, and in view when parsing the values). Should I move SelectItem populating logic into view as well, and keep my business objects in the view model instead?

It also bugs me that in rare case when JavaScript is disabled, the user will not be able to sumbit the form at all.

Option 2

Expose a unique "Kind_RecordId"-like string property in my form model, and let the model parse it (or fill it) through strong-typed accessor properties for the controller?

This seems like a better way but I'm not entirely sure. What I consider good about it is it'll keep all parsing/concatenating logic in view model, and it will not fail when JavaScript is not available.

Community thoughts are very appreciated.

Community
  • 1
  • 1
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • So will users be selecting from any combination of Kind and RecordId? Meaning, if there are 3 kinds and 4 record ids they can select from 12 drop down items? – Lester Jun 03 '11 at 14:50
  • No, there won't be two different lists at all. You can think of it as of a composite key. – Dan Abramov Jun 03 '11 at 15:05
  • That's what I meant. 1 drop down list with 12 items. It's every combinations of kinds and record ids in 1 dropdown. Correct? – Lester Jun 03 '11 at 15:07
  • No. Closer to real world: a *user* can message another *user* or *store*. Therefore, there'll be a property for entity ID, and a property for entity *kind*. It's not overkill because there are also other kinds of entities that can receive messages, and all of them have their own tables. A user can choose any target user/store/whatever for `To` field, however it doesn't make any sense to let user manually select kind and ID. – Dan Abramov Jun 03 '11 at 15:27
  • What I mean is there can be e.g. 6 options available, like 4 users and 2 stores. Or 5 stores and one user. Or all of them can be users.. It depends on the data from database. It's never the "every combination" type of thing. – Dan Abramov Jun 03 '11 at 15:29

2 Answers2

2

An elegant way to do this is to create your own model binder and a separate class to hold Kind and RecordId as properties. You'll need to specify model binder for your class with an attribute, and you'll then be able to use the class as an action method parameter.

[ModelBinder (typeof (RecordIdentityBinder))]
public class RecordIdentity
{
    // Your types may be different
    public RecordKind Kind { get; set; }
    public int RecordId { get; set; }

    //TODO: replace this with your formatting algorithm
    public override string ToString ()
    {
        return string.Format ("{0}_{1}", Kind, RecordId);
    }

    //TODO: replace this with your parsing algorithm
    public static RecordIdentity Parse (string s)
    {
        string[] fragments = s.Split('_');

        if (fragments.Length != 2)
            return null;

        // Your object creation may be different
        return new MessagePartyIdentity {
            Kind = (RecordKind) Enum.Parse (typeof (RecordKind), fragments [0]),
            PartyID = int.Parse (fragments [1])
        };

    }
}

public class RecordIdentityBinder : IModelBinder
{
    public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext)
    {            
        string key = bindingContext.ModelName;
        ValueProviderResult value = bindingContext.ValueProvider.GetValue (key);

        if (value == null || string.IsNullOrEmpty (value.AttemptedValue))
            return null;

        return RecordIdentity.Parse (value.AttemptedValue);
    }
}

public class MyController : Controller
{
    // ?identity=Foo_42 will become RecordIdentity { Kind = RecordKind.Foo, RecordId = 42 }
    public ActionResult DoSomethingWith (RecordIdentity selectedIdentity)
    {
        // ...
    }
}

In the view template code, you'll need to call Html.DropDownListFor() with your composite property:

@Html.DropDownListFor(m => m.SelectedIdentity, Model.RowList)

For each value in RowList or whatever you call your data source, make sure SelectListItem's Value property is set to you class' ToString() output.

The name of object's RecordIdentity-typed property specified in Html.DropDownListFor should match action parameter name. Otherwise you'll need to specify dropdown name in Bind attribute's Prefix property as seen here:

public ActionResult DoSomethingWith ([Bind(Prefix="SelectedIdentity")] RecordIdentity identity)
{
    // do something you need
}

Also note that it will also work if you want RecordIdentity to be a property of form model or whatever class that you'd like to accept as a parameter for your action method. The binder will get called either way.

By following this approach, you make single class responsible for formatting and parsing which is largely invisible and provides an abstraction over actual fields. However I need to note that such problem may as well be caused by poor database or business entities organization. Maybe a better solution would be to provide each of the items you listed with its own unique native key.

Community
  • 1
  • 1
David Levin
  • 6,573
  • 5
  • 48
  • 80
  • thanks for your input. This looks like a cool approach so I'll try it out and let you know. As for entities organization, these entities are actually from different tables, and they only make sense to be generalized in a very specific situation. This is why I decided to go with such "composite key" to describe them. – Dan Abramov Jun 03 '11 at 15:35
  • I'm not sure that code of BindModel method is correctly works, but if you do all other suggested actions to activate this binder - you can debug it at created infrastructure and told me, if something incorrect. – David Levin Jun 03 '11 at 15:41
  • this turned out to be a perfect solution, thanks a lot. I just updated your answer with 100% working code and a small addition. – Dan Abramov Jun 03 '11 at 18:26
  • can you please check the `Bind` attribute part for correctness? I tried to provide a real world example but I'm not 100% I got this one right. – Dan Abramov Jun 03 '11 at 18:34
  • 1
    @gaearon, ok, i'll check it today for full confidence, but model binding mechanism based only on names and values that passed through form collection by POST. If you bind simple class with props and another parameter name - it successfully filled, because at form you have names "myEntity.Id", "myEntity.Value" - and props names of parameter class is identical, and object created by default ctor. But if you bind parameter with primitive type (it haven't props to fill) - you need exact names match. – David Levin Jun 03 '11 at 22:51
1

I think it best to define a new class with 2 properties. Something similar to:

public class KindRecordItem {
    public int Id {get; set;}
    public string Kind {get; set;}
    public int RecordId {get; set;}
    public string Text {get; set;}
}

You generate all valid / legal combinations of kind and recordId into a collection, pass that collection to the view, and display the display text in the drop down.

<%= Html.DropDownListFor(m => m.SelectedKindRecordItem, 
        new SelectList(Model.KindRecordItems, "Id", "Text"),
        new SelectListItem { Value = "0", Text = "Please select an option" }) %>

In your controller, SelectedKindRecordItem will be a unique ID that you can look up to find the Kind and RecordId for processing.

Id can be generated as a hash of Kind and RecordId or any other method to make it unique.

Lester
  • 4,243
  • 2
  • 27
  • 31