MVC Component with postback

749 views Asked by At

I am trying to build a set of MVC components which can be easily reused with various settings. They are built and used as child actions, because I need them to be able to make partial postback to themselves without the knowledge of the other content of the view on which they are hosted.

I am facing an issue with where to store their parameters without passing them through client (I don't want to build something like view state, and also for security reasons), and make them available to the partial postback.

Here is a simplified example (not that code may not be compilable as I cut my syntax sugar to simplify it to plain MVC):

View code (component usage):

@Html.Action(
    "Default",
    "FacebookFeed",

    new {
        // I don't want this data to pass through client
        settings = new FacebookFeedSettings {
            AppKey = "XYZ",
            AppSecret = "123",
            PageSize = 10
        }
        .ItemTemplate(
            @<div class="feed-item">@item.Title</div>
        )
    }
)

Controller code:

public class FacebookFeedController {
   public ActionResult Default(FacebookFeedSettings settings)
   {
      // Action code using settings

      return PartialView(model);
   }
}

Feed view code:

@using (Ajax.BeginForm("Default", "FacebookFeed", new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.ReplaceWith }))
{
    // Form code

    <input type="submit" value="Refresh" />
}

So when the Refresh button is hit, it is supposed to render fresh data, but settings is missing in that request.

So far I came up only with solution to make some sort of settings register indexed by string key where they would register their settings set.

Modified view code (component usage):

@Html.Action(
    "Default",
    "FacebookFeed",
    new {
        settingsKey = "HomePageFBFeed"
    }
)

Extra code from user:

[ComponentSettings]
public class HomePageFBFeed : FacebookFeedSettings
{
    public HomePageFBFeed()
    {
        AppKey = "XYZ";
        AppSecret = "123";
        PageSize = 10;
    }
}

Modified controller code:

public ActionResult Default(string settingsKey)
{
   FacebookFeedSettings settings = ComponentSettings.GetSettings(settingsKey);

   // Action code using settings

   return PartialView(model);
}

Modified view code:

@using (Ajax.BeginForm("Default", "FacebookFeed", new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.ReplaceWith }, new { settingsKey = Model.SettingsKey }))
{
   ...
}

So it this case I pass over the client only some unique ID of that configuration, which is fine, but it has lousy user experience compared to first as it needs to be managed outside of view where the component is placed.

I am also not able to use inline template in this case as shown in the first code part, because in this case settings is built outside the scope of a view.

Note that I also need this to work with application restarts, and across process boundaries (in cloud), so I can't rely on storing the configuration on server side at the first load of the view.

Is there some better way / best practice how to do that in ASP.NET 4.6 / MVC 5?

If not, will it be possible in ASP.NET 5 / MVC 6?

2

There are 2 answers

1
Stefano Tempesta On

Martin, I know you said you don't want to create a view state, but considering the disconnected mode of MVC (you don't want to use server-side Session, also because it doesn't scale), would you be open to transfer an encrypted string of the settings?

Something like this in the view:

@using (Ajax.BeginForm("Default", "FacebookFeed", new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.ReplaceWith }))
{
    @Html.AntiForgeryToken()
    <input type="hidden" name="Settings" value="@Model.ToEncryptedString()" />
    <input type="submit" value="Refresh" />
}

The ToEncryptedString() would be an extension on your model, that:

  1. Serialize your settings (the FacebookFeedSettings object)
  2. Encrypt it
  3. Convert to Base 64 to make it HTTP friendly

When going back to the controller, all you need to do is to read the Settings parameter in the Default action:

[HttpPost, ValidateAntiForgeryToken]
public ActionResult Default(string settings)
{
    FacebookFeedSettings facebookSettings = FacebookFeedSettings.FromEncryptedString(settings);
    // do something with the settings
    // build the model
    return PartialView(model);
}

The FromEncryptedString() does exactly the opposite direction:

  1. Convert from Base 64 to byte[]
  2. Decrypt the byte array
  3. Deserialize to a FacebookFeedSettings object instance

Basically, this encrypted string works more or less like the anti-forgery token. To make it even more elegant, I guess you can move the settings validation also at attribute level, and mark your action with a custom attribute:

[HttpPost, ValidateAntiForgeryToken, FacebookFeedSettings]
public ActionResult Default(/* No need to capture settings here */)

The FacebookFeedSettingsAttribute would obtain the Settings parameter from the Request and the build and validate the FacebookFeedSettings object instance. I haven't tried going this far in my try, I leave it to you to practice :-)

What do you think?

5
Stefano Tempesta On

I understand and I agree, the problem is that you'd like to keep some form of state anyway, in a stateless technology.

Good news is that MVC 6 seems to have the answer to your problem. First of all, child actions do not exist any longer, replaced by View Components. View Components are made of a C# class and a Razor view, and do not have a dependency on a Controller, which eases reusability.

I do not have a direct experience yet, but from what I am reading about MVC 6 and specifically View Components, they are self-contained, so basically you can manage state within the components itself. Parameters are not passed over HTTP from the view, because you actually invoke the component server-side (no trip back from the client).

Also, View components don’t take part in the controller lifecycle but you still have access to ViewBag and ViewData (shared with the Controller). Lasty, like controllers, View Components also take part in dependency injection so any other information you need can simply be injected to the view component.

This should work for you, but you need to wait for MVC 6 :-)