2014. február 1., szombat

MVC: Egyszerre több partial view frissítése

Online demo és letöltés: pulzonic.com/multipartial
Nuget csomag: www.nuget.org/packages/Multipartial

Bevezetés


Nemrégiben volt egy projektünk, amelyen nagyon kevesen dolgoztunk fejlesztők. Fontos szempont volt, hogy a webalkalmazásunknak Facebook-szerű, komplex UI-a legyen, de semmiképpen sem szerettünk volna nekiállni bonyolult kliensoldali kódot írni, ezért elhatároztuk, hogy minimalizáljuk a JavaScript mennyiségét, és lehetőleg minden html kódot a szerveroldalon renderelünk ki. Az elképzelésem az volt, hogy a Html tartalmat felszabdaljuk sok, egymásba ágyazott partial view-ra (akár egy lista egyetlen listaeleme is egy partial view lehet), ezeket view-kat frissítjuk, így mindössz annyi JavaScriptre lesz szükségünk, ami elposztolja a kérést, majd a visszakapot html snipetet egyszerűen a helyére rakja.

Az gyorsan kiderült, hogy ez a beépített ActionResult-okkal nem fog menni, ezért csináltam egy saját megoldást.



A megoldás


Készítettem egy saját AcionResult osztályt, amely segítségével egyszerre lehet a weboldalnak több, egymástól teljesen független részét frissíteni. Ennek a neve MultipartialResult lett.

Tegyük fel, hogy egy webes levelező alkalmazáson dolgozol. Amikor itt a felhasználó kiválaszt egy levelet, és rákattint a Törlés gombra, a következő dolgok történnek egyszerre:

  • Az aktuális kijelölt levél eltünik a listából, és a következő jelölődik ki.
  • Az Előnézet dobozban az újonnan kijelölt levél tartalma jelenik meg
  • A levelek számát jelző szám megváltozik
  • A böngésző címsora is megváltozik, mivel ott is fel van tüntetve az olvasatlan levelek száma
Ebben az esetben mind az email-ek listája, mind az Előnézet doboz egy-egy partial view. A levelek száma egy egyszerű szöveg a DOM-ban.

A MultipartialResult használatával a Delete gomb eseménykezelő action-ja valahogy így néz ki:

public ActionResult OnDelete(long EmailId)
{
     //... does the deleting
     //... creates the model for the new InboxList partial view (InboxListModel)
     //... creates the model for the PreviewPane partial view (PreviewPaneModel)
     //... calculates the number of the emails (EmailCount)
     //... renders the browser title with the updated unread email number (BrowserTitle)
    
     MultipartialResult result = new MultipartialResult();
     result.AddView("_InboxList","InboxListDiv",InboxListModel);
     result.AddView("_PreviewPane","PreviewDiv",PreviewPaneModel);
     result.AddContent(EmailCount.ToString(),"EmailCountDiv");
     result.AddScript(string.Format("document.title='{0}';",BrowserTitle));
     return result;
}

Az AddView eredményeként az _InboxList view tartalma az InboxListDiv elemben fog megjelenni.
Az AddContent eredményeként a megadott string az EmailCountDiv-be kerül bele.
Az AddScript eredményeként a kliensoldalon lefut a megadott JavaScript.

A kliensoldalon az egyetlen teendő van, meghívni a MultipartialUpdate JS függvényt az OnSuccess eseménykezelőben:

@Ajax.ActionLink("Delete", "OnDelete", new { EmailId = Model.CurrentEmail.Id }, new AjaxOptions { OnSuccess = "MultipartialUpdate" })

vagy, Ajax formban így:

@using (Ajax.BeginForm("OnDelete", 
 new { EmailId = Model.CurrentEmail.Id }, new AjaxOptions { OnSuccess = "MultipartialUpdate" }))

vagy, jQuery .post vagy .ajax függvényben így:

function deleteClicked(emailId) { 
    $.ajax({
        url: "/inbox/ondelete",
        type: "POST",
        data: { emailId: emailId },
        success: function (result) {
            MultipartialUpdate(result);
        },
    });

Háttér


A Multipartial működési elve nagyon egyszerű. A JsonResult osztályból van örököltetve, lerendereli a megadott view-kat stringekbe, ezeket összegyűjti, és egy json-ba csomagolva elküldi a kliensoldalra. Ott pedig egy JS függvény szépen végigmegy rajtuk, és mindegyiket a helyére rakja a DOM-ban.

Amikor a szerveroldalon összegyűjti, mindegyik megjelöli tartalom alapján:


public MultipartialResult AddView(string viewName, string containerId, object model = null)
{ 
        views.Add(new View() { Kind = ViewKind.View, 
        ViewName = viewName, ContainerId = containerId, Model = model });
  return this;
}
public MultipartialResult AddContent(string content, string containerId)
{
  views.Add(new View() { Kind = ViewKind.Content, Content = content, ContainerId = containerId });
  return this;
} 
public MultipartialResult AddScript(string script)
{ 
  views.Add(new View() { Kind = ViewKind.Script, Script = script });
  return this;
}

Mikor az action visszatér az ActionResult osztállya, az Mvc keretrenszer meghívja az ExecuteResult függvényt, amely elkészíti ezt a json-t:


public override void ExecuteResult(ControllerContext context)
        {
            List<object> data = new List<object>();
            foreach (var view in views)
            {
                string html = string.Empty;
                if (view.Kind == ViewKind.View)
                {
                    //view result
                    html = RenderPartialViewToString(mController, view.ViewName, view.Model);
                    data.Add(new { updateTargetId = view.ContainerId, html = html });
                }
                else if (view.Kind == ViewKind.Content)
                {
                    //content result
                    html = view.Content;
                    data.Add(new { updateTargetId = view.ContainerId, html = html });
                }
                else if (view.Kind == ViewKind.Script)
                {
                        //script result
                    data.Add(new { script = view.Script });
                }
            }
            Data = data;
            base.ExecuteResult(context);
        }


A view-k string-be renderelésébe most inkább nem mennék bele...

A kliensoldalon már csak egy kis egyszerű JS függvényre van szükség, amely megkapja ezt a json-t, végigmegy rajta és mindegyik elem esetében frissíti a DOM-t, vagy futtatja a JS-t.


function MultipartialUpdate(views) {
    for (v in views)
        if (views[v].script) {
            eval(views[v].script);
        }
        else {
            $('#' + views[v].updateTargetId).html(views[v].html);
        }
    return false;
}

Remélem, van még rajtam kívül valaki, aki ezt hasznosnak találja... :)

Nincsenek megjegyzések:

Megjegyzés küldése