Wednesday, March 27, 2013

I miss ViewState...


So I am using MVC and Razor now and I frequently run into the situation where I want to have a model that gets posted, but this model contains data that I don't want to expose to change. So there is this Html.HiddenFor where you can put data that won't be displayed. The problem is that you have to create a hidden input for every field in your model you don't want to display. Sometimes this is painful. Also these inputs are fairly easy for people to modify and this creates insecurity in your system.

Well, this particular problem has already been solved. WebForms uses ViewState, a compressed and potentially encrypted piece of data. The annoying thing about ViewState was that it took all sorts of data and automatically jammed it into ViewState instead of just doing a 'model'. There was no model concept so it just stored a bunch of stuff. Also another problem with ViewState was that it was very limited to the page because it had a lot of control hierarchy data that did not make sense on another page. In MVC the model binding system is by model and not based on control hierarchy so you can simply post to any page that binds to the model object you are using.

So what would be cool is ViewState for a model or should I say ModelState since it no longer encapsulates view information. These terms are overloaded a bit so my apologies.

So let's come up with a generic solution. The first step is to serialize your entire model.  Then you compress it and encrypt it. 

The code below is simple compression:

public class ModelCompressor
{
    public static string Compress(object model)
    {
        return Zip(ToString(model));
    }

    public static object Decompress(Type modelType, string model)
    {
        return FromString(modelType, UnZip(model));
    }

    private static string ToString(object model)
    {
        XmlSerializer serializer = new XmlSerializer(model.GetType());
        StringBuilder stringBuilder = new StringBuilder();
        using (TextWriter writer = new StringWriter(stringBuilder))
        {
            serializer.Serialize(writer, model);
        }
        return stringBuilder.ToString();
    }

    private static string Zip(string value)
    {
        byte[] byteArray = new byte[value.Length];
        int indexBA = 0;
        foreach (char item in value)
        {
            byteArray[indexBA++] = (byte)item;
        }
        using (MemoryStream memoryStream = new MemoryStream())
        {
            using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Compress))
            {
                gZipStream.Write(byteArray, 0, byteArray.Length);
            }
            byteArray = memoryStream.ToArray();
        }
        return Convert.ToBase64String(byteArray); 
    }

    private static object FromString(Type modelType, string model)
    {
        object retval;
        XmlSerializer serializer = new XmlSerializer(modelType);
        using (TextReader reader = new StringReader(model))
        {
            retval = serializer.Deserialize(reader);
        }
        return retval;
    }

    public static string UnZip(string value)
    {
        byte[] byteArray = Convert.FromBase64String(value);
        StringBuilder stringBuilder;
        using (MemoryStream memoryStream = new MemoryStream(byteArray))
        {
            int rByte;
            using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
            {
                byteArray = new byte[byteArray.Length];
                rByte = gZipStream.Read(byteArray, 0, byteArray.Length);
            }
            stringBuilder = new StringBuilder(rByte);
            for (int i = 0; i < rByte; i++)
            {
                stringBuilder.Append((char) byteArray[i]);
            }
        }
        return stringBuilder.ToString();
    }
}

Then you shove this into a hidden input:
@Html.Hidden(Model.GetType().ToString(), ModelCompressor.Compress(Model))


Now to the model binding. Here is the setting of the binder:
ModelBinders.Binders[typeof(FruitModel)] = new FruitModelBinder();
Normally the above goes in Global.asax.cs but there are ways to place it in a more generic place, but I don't remember offhand (WebActivator?). Also it would be nice to do this in a more generic way. Will think on it later...

Now the binder itself:
public class FruitModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        string modelName = bindingContext.ModelType.FullName;
        FruitModel fruit = (FruitModel)ModelCompressor.Decompress(bindingContext.ModelType, 
            bindingContext.ValueProvider.GetValue(modelName).AttemptedValue);

        DefaultModelBinder modelBinder = new DefaultModelBinder();
        FruitModel fruit2 = (FruitModel) modelBinder.BindModel(controllerContext, bindingContext);
        return fruit;
    }
}


Okay, I admit this isn't in a working state yet. It generates two models. One is the original model complete with any data you didn't necessarily include in the page. The second is the standard model generated by Razor. I just need to combine them correctly which isn't as trivial as I thought.

The other problem is that this has some very specific pieces tied to my FruitModel type and I need to get rid of this specificity.

So this needs some work, but the core idea is here. If I ever finished this I will github and nuget it.

No comments:

Post a Comment