3

With the following simple action...

[HttpPost]
public PartialViewResult DoSomething(AnimalInfo animalInfo)
{
    // animalInfo.AnimalKey SHOULD == DogKey { Id = 1, Name = "Dog" }
    // BUT animalInfo.AnimalKey == null

    return Something();
}

Posting to this action is fine and most of animalInfo's properties are available, except for an object I created myself. I was assuming it was a Serialization issue, so I added some basic Serialization to my class but I still get null for the AnimalKey object (which is definitely not null at render time).

Here is how I define AnimalInfo:

[DataContract]
public class AnimalInfo : IAnimalInfo
{
    [DataMember]
    public IAnimalKey AnimalKey { get; set; }

    public string Name { get; set; }
}

[DataContract]
public class DogKey : IAnimalKey
{
    public DogKey(int id){ DogId = id; }

    [DataMember]
    public int DogId { get; set; }
}

And I post to the Action from a view like this...

<% var currentAnimal = new AnimalInfo { AnimalKey = new DogKey(1), Name = "Dog" }; %>
<% using (Html.BeginForm("DoSomething", "Controller", currentAnimal.AnimalInfo))
{
    %><button type="submit">post</button><%
} %>

But by the time my Action is executed, AnimalKey has become null while Name is "Dog". Looking into ModelState reveals the same. It does seem like a serialization issue. Is this the case? If so, is DataContract and DataMember not sufficient enough a method to handle this?


UPDATE

As an alternative approach, I tried changing it to a strongly typed view and using HiddenFor's for posting the data, but I still get exactly the same result.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Namespace.IAnimalInfo>" %>

<% using (Html.BeginForm("DoSomething", "OM", Model))
{
    Html.HiddenFor(m => m.AnimalKey);
    Html.HiddenFor(m => m.Name);
    %><button type="submit">revert</button><%
} %>
  • Do MVC HtmlHelpers not serialize complex objects at all?

  • Or do I need to create a different Model for each AnimalKey in order to get these values back?

  • Or even write my own serialize to a string for a hidden and then deserialize it on its return trip?


PROBABAL CAUSE

After a bit more fiddling I'm pretty certain that it is due to IAnimalKey being an interface. When I added another property called DogKey to my model, the value is returned. So it seems that it lost its knowledge of what type of object it was on the post or doesn't support interfaces at all.


DIRTY FIX

See my answer

Arkiliknam
  • 1,805
  • 1
  • 19
  • 35
  • 1
    I don't think you can pass a rich object into `BeginForm` like that. The object is used to create a `RouteValueCollection` which is just a flat dictionary, not a full object tree. – bhamlin Mar 01 '12 at 19:43
  • If you look at the HTML generated by Html.HiddenFor(m => m.AnimalKey);, what does it look like - what is the name of the generated hidden field and what is its value? – Rune Mar 02 '12 at 11:59
  • I'm just facing a similar situation... no matter what I try a `ConcurrencyCheck` property of type `byte[]` won't be added to the `ModelState`. Guess what? It's declared in an interface and then declared on each `POCO` object. I'll have to create `ViewModels`. There's no other way around it out of the box. I think `ViewModel` is the way to go. More code on the way... :D Anyways you did some hard working code! Congrats... – Leniel Maccaferri Aug 09 '14 at 03:31

4 Answers4

2

Haven't received any answers to the specific issue, possibly because it is not supported by ASP.NET MVC, though I couldn't find any confirmation on MSDN either. So here is my dirty fix for lack of a better answer. Basically, I turned AnimalInfo into an abstract class and implemented what I called Serialize and Deserialize methods which transform each AnimalKey into a string that can be used to recreate the AnimalKey at a later time.

This works because in my new AnimalInfo class which I use as my MVC Model, I have a property for the SerializedKey which on Get attempts to Serialize the AnimalKey. Using an HtmlHelper to store this propery will then cause the Get to perform the serialization, storing the product in the rendered view. The Set method will attempt to Deserialize the provided string to an AnimialKey. So when a Post Action occurs, MVC attempts to Set the SerializedAnimalKey and by doing so calls Deserialize and recreates the AnimalKey object with all that I intended to serialize.

Would love to see some other answers to this specific issue, but for now this is what I have to go with.

public abstract class AnimalKey
{
    public abstract AnimalType AnimalType { get; }
    public abstract string Serialize();

    public static AnimalKey Deserialize(string serializedKey)
    {
        var split = serializedKey.Split('|');
        switch ((AnimalType)Convert.ToInt32(split[0]))
        {
            case AnimalType.Dog:
                var dogKey = new DogKey();
                var dogProperties = split[1].Split(',');
                dogKey.DogId = Convert.ToInt32(dogProperties[0]);
                return dogKey;
            // TODO: Other key implementations
        }
    }
}

public class AnimalInfo : IAnimalInfo
{
    public AnimalKey AnimalKey { get; set; }

    public string SerializedAnimalKey 
    {
        get { return AnimalKey != null 
            ? AnimalKey.Serialize() 
            : string.Empty;
        }
        set { AnimalKey = String.IsNullOrEmpty(value) ? null : AnimalKey.Deserialize(value); }
    }       
}

public class DogKey : AnimalKey
{
    [DataMember]
    public int DogId { get; set; }

    public override AnimalType AnimalType
    {
        get { return AnimalType.Dog; }
    }

    public override string Serialize()
    {
        var serializeBuilder = new StringBuilder();
        serializeBuilder.AppendFormat("{0}|", (int)AnimalType);
        serializeBuilder.AppendFormat("{0},", DogId);

        return serializeBuilder.ToString();
    }
}
Arkiliknam
  • 1,805
  • 1
  • 19
  • 35
  • It does open up other potential issues down the track, but it gets the job done for this relatively simple case. – Arkiliknam Mar 06 '12 at 09:09
1

Remember that @Html.Hidden and @Html.HiddenFor are just helpers to create this

<input type="hidden" value="yourValue" name="yourName" />

Hidden field is very helpful for strings, numbers, Guid etc. Placing a complex object on it will probably do ToString operation on it. You can use browser's developer toolbar (F12 normally) to inspect what the input contains.

I have to admit that I have normally done this kind of things using JavaScript (with jQuery). Because custom bindings tend to get complex. What I do is that I have a view model that I render to the screen and on submit I'll gather required information and do $.ajax or $.post.

Maybe someone else can give you ideas how to implement custom binding.

Tx3
  • 6,796
  • 4
  • 37
  • 52
  • As you mentioned, I think the HtmlHelper was most likely just calling ToString on my object... so it seems only the simplest of value types are supported out of the box. I implemented my own Serialize to output a string and used that to store and reconstruct my information instead. – Arkiliknam Mar 05 '12 at 14:37
  • @Arkiliknam interesting solution. I stumbled on many binding issues and finally gave up and moved to JSON communication between client (jQuery) and server (ASP.NET MVC). – Tx3 Mar 06 '12 at 07:37
  • ASP.NET MVC Model Binding is fantastic when it works, but it can be a pain some times when attempting to do things slightly different. I have also gone the route of jQuery and JSON many a time to handle these issues but do prefer to keep things .NET when possible. – Arkiliknam Mar 06 '12 at 09:07
0

you can't get things done when using

<% using (Html.BeginForm("DoSomething", "Controller", currentAnimal.AnimalInfo)) 

put model object in BeginForm method will put serialized string into form query string, and it only support simple plain object, even collections are not supported.

You can try this

@using (Html.BeginForm()) 
{
    @Html.TextBoxFor(o => Model.Name);
    @Html.TextBoxFor(o => Model.AnimalKey.DogId);
    <button type="submit">post</button>
}

Values will pass to server in post data and it works.

Teddy
  • 1,407
  • 10
  • 19
  • Problem is my AnimalKey is of IAnimalKey and I don't know whether it contains DogId or CatId so cant explicitly write HtmlHelpers for them. This may be a limitation I wasn't aware of before. – Arkiliknam Mar 05 '12 at 09:31
  • Is that possible to include an ID in IAnimalKey, or maybe also a property "Type"? I think it will make things simple. – Teddy Nov 28 '12 at 02:33
0

I think you have a basic misunderstanding of the MVC model.

What you are describing, deserializing an instance and storing on the client-side is the Web Forms way of doing things. Instances are serialized to the ViewState and when the page posts back, the instance data is rehydrated using the ViewSource.

Something like this:

public AnimalInfo animalInfo
{
    get
    {
        return ViewState("AnimalInfo")
    }
    set (AnimalInfo value)
    {
        ViewState("AnimalInfo") = value
    }
}

What happens here (in the Web Forms world) is that the instance is serialized to the ViewState in the set and deserialized from the ViewState in the get.

This mechanism does not exist in MVC.

First there is no ViewState mechanism. This is actually one of the major advantages of MVC, because it results in a page that weighs a lot less than a Web Forms page. This is mostly due to the size of the ViewState. And everybody wants a lighter page :)

Second, the concept of PostBack also does not exist in MVC. Once a page is sent to the client, that's it.

What MVC does do is utilize the original web methods of POST and GET.

Passing information is done one of two ways, via the querystring (GET) or via a form (POST).

All that being said, here are some of the MVC ways of doing what you want.

If this is an edit/update form, you can use Html.EditorForModel. This will create a form field for each property of your ViewModel. Not only will it do that, but if you wire up your Controller correctly ([HttpPost] public PartialViewResult DoSomething(AnimalInfo animalInfo)), MVC will crawl the POSTed form and reconstruct the instance for you from the form field values and assigned the new instance to the animalInfo variable.

If this is not an edit/update form, then why do you need the Controller to rehydrate the instance?

There are also ways to hydrate only certain properties of an instance, like @HiTeddy suggested (@Html.TextBoxFor(o => Model.Name);). That would require some additional wiring on the Controller to tell it where to retrieve the property values from.

Shai Cohen
  • 6,074
  • 4
  • 31
  • 54
  • I'm not sure you understood the question. I have Model and understand what it is. I am attempting to post a model with one property that happens to be of type IAnimalKey. When I do a post using a form for example, only this property becomes null. It will however work if I swap out my IAnimalKey for the concrete class DogKey. But I didn't want to do this as I have many IAnimalKey class. – Arkiliknam Mar 05 '12 at 09:07
  • And if there is anything I wanted to POST to my controller, it would have to be serialized into the rendered view in order to do so, in most cases a simple .ToString() will suffice, otherwise all knowledge of the model will be lost. – Arkiliknam Mar 05 '12 at 09:19