Dynamic JS array built using Projection / Lists

Topics: Customizing Orchard, Writing themes
Nov 26, 2012 at 7:28 AM
Edited Nov 26, 2012 at 7:52 AM

I am really new to Orchard and what I'm attempting to do is create a list from anything that has a very specific Part attached then take a field of that part and display it statically right next to eachother. The reason for this is I have an AJAX driven site that I need to dynamically build a JS structure so the hyperlink anchors can be called and a dynamic content screen can then show the requested content item. When a user adds more content I need that JS structure to be changing with it. So far I've got everything working including partial pages doing some work with the Document and Layout files but trying to dynamically create this array is becoming very difficult since Lists and Projections appear to want to put some form of formatting or a hyperlink with it.

Below is an example of the JS structure I'm attempting to build dynamically by detecting a specific Part in a Content Item.  Each line in this array could be handled with a single field, if necessary on the Part.  Any guidance to what the easiest solution to this fairly trivial problem is is appreciated :)

		    var page = {
		        "about": { "js": "page-1.js", "html": "Partial/About", "title": "About" },
		        "services": { "js": "page-2.js", "html": "Partial/Services", "title": "Services - Legal Services" },
		        "clients": { "js": "page-3.js", "html": "Partial/Clients", "title": "Clients - Testimonials" },
		        "practice": { "js": "page-4.js", "html": "Partial/Practice", "title": "Practice - Practice Of Law" },
		        "contact": { "js": "page-5.js", "html": "Partial/Contact", "title": "Contact - Get In Touch" }
		    };
 
Coordinator
Nov 26, 2012 at 8:27 AM

It's not super-clear to me how you are trying to use projections here. Do you have a custom layout outputting JSON?

Nov 26, 2012 at 5:44 PM
Edited Nov 26, 2012 at 5:47 PM

What I've done is I have 3 Zones... a Header zone, Content zone, and Footer zone.  The Header zone has some static HTML for displaying the top page bar with a logo and slogan, etc.  The Content zone is located in a div right in the center of the page but by default has no content at all because this is loaded through Javascript 'getURL' calls.  I edited the Layout.cshtml to do a test on the URL to where if it see's a url with the partial url '/Partial/' it will not render any of the <head> through the start of the Content zone along with whatever proceeds after it down to </html>.  The reason for all of this is as a user clicks a link such as "#!about" a javascript will dynamically go get a page called '/Partial/About' and render that in a small window without any other tags.  This allows me to have a homepage that never changes and the content changes based on the content items setup.  If I want content to render normally then I just leave the url '/Partial/' off when I'm setting up the routing and it will render as a normal postback page.  I haven't started messing with the Web API yet and my hunch this would be trivial to fix with it but this was a pre-built template that I'm trying to weld onto the CMS framework.

Now enter the problem... everything works great but that array needs to be updated dynamically to have the URL's that can be dynamically loaded into the Content div window.  I thought about just using Projections to create a list of fields from detected parts so, for example, for each page that has the 'LiteralPart' attached, it would render whatever is in this Literal part straight to the user with no formatting at all.  This would allow me to put a projection in the footer basically building an array with no html tags etc.

 I apologize in advance if this isn't very clear. 

Nov 26, 2012 at 6:26 PM

What part of this are you having trouble with? It's still not clear exactly what you tried, and what your question is. What exactly does the Content Item / Content Parts look like that you are trying to display? You need all of it as JSON, correct? Does it need to go out as JSON with the initial HTML, or are you always grabbing the JSON from the server via AJAX (I think this is what you are doing but just making sure). 

Nov 26, 2012 at 7:27 PM

JSON is not being used no so I mispoke earlier about AJAX.  I'm just using Javascript to pull a URL that happens to be a content item.  I need the Projector to give me some content results that, if I have a parts on 3 random items that contain that part that gives a single field with data  (eg. "Book1URL", "Book2URL", "Book3URL") that when the projector goes to show the list it displays as: Book1URLBook2URLBook3URL.  Nothing else needs to be done.  If I can achieve this, then I can hook up the array.  I don't want any grid, ul / li, etc.  Just plain right next each other.

Nov 26, 2012 at 7:44 PM

 I still don't know what your question is. Maybe you could post a more concrete example of what you want to do, and include code for the content items/parts that are involved, and for the javascript that is pulling these "URL that happens to be a content item". What's the result of that javascript call? Html? Json? A javascript object/array, etc? The more specific you are, and the more sample code you include, the better. 

Then, indicate what exactly your question is. Now it's sounding like the part you want help with is getting the data, but it's not clear what the criteria is for pulling the data, or what data is being pulled. Have you already defined the content items? 

Nov 26, 2012 at 8:11 PM
Edited Nov 26, 2012 at 8:18 PM

Basically what I need is for the javascript array down below to be built and I really don't care how I do it by it has to be dynamic and has to be in the Footer Zone. Each line in the array represents a hyperlink to a page of content.  All we care about is the text is displayed exactly as it is below line by line.  This array is normally static but because Orchard is going to allow the user to create content pages of their own, I need this array to update with a new line for each content item such as a 'Page'.  My idea was to set a Field or Part to a Content Item such as Page and then pull that field for each Content Item to list by way of a Projector.  I could do this very easily in a basic for loop in code but I'm trying to use Orchard to accomplish this using its own listing mechanisms plus I'm not too keen on getting at the database through manual means.  If SQL was an option a simple query selecting each content item that had this specific part or field assigned to it, then display would accomplish this.  Maybe Projections isn't the best way to handle this.  The best way to see each of these lines in this array is as a form of Hyperlink... but it has to be in the format below.  This is a datastructure that a javascript method uses to know how to pull a remote file and display it.

		    var page = {
		        "about": { "js": "page-1.js", "html": "Partial/About", "title": "About" },
		        "services": { "js": "page-2.js", "html": "Partial/Services", "title": "Services - Legal Services" },
		        "clients": { "js": "page-3.js", "html": "Partial/Clients", "title": "Clients - Testimonials" },
		        "practice": { "js": "page-4.js", "html": "Partial/Practice", "title": "Practice - Practice Of Law" },
		        "contact": { "js": "page-5.js", "html": "Partial/Contact", "title": "Contact - Get In Touch" }
		    };

I apologize if I'm still not being more clear...

Coordinator
Nov 26, 2012 at 9:14 PM

If you want to build JSON from the results of a projection, you should build a projection layout. Look at the code for the existing ones. You can also watch my talk on projections that explains how to build your own layout. http://www.youtube.com/watch?v=Ka55wTTXZg8&list=UUIo5Bf9vIjfkzRRFKaT_G6Q&index=4&feature=plcp

Nov 27, 2012 at 6:30 PM

I'll check that out.  Thank you so much :)  What I ended up doing just to get past this was I made a Web API controller in a custom module and queried the database:

    public class ContentItemArrayController : ApiController {
        public ContentItemArrayController(IOrchardServices orchardServices) {
            Services = orchardServices;
        }

        public IOrchardServices Services { get; set; }
        public Localizer T { get; set; }

        public HttpResponseMessage Get() {
            string msg = String.Empty;

            IEnumerable<ContentItem> pages = Services.ContentManager.Query(VersionOptions.Published, "Page").List();
            IEnumerable<ContentItem> aboutPages = Services.ContentManager.Query(VersionOptions.Published, "AboutPage").List();
            pages = pages.Union(aboutPages);

            foreach (ContentItem page in pages) {
                ContentItemMetadata meta = Services.ContentManager.GetItemMetadata(page);
                string alias = meta.Identity.Get("alias");

                if (alias.Contains("Partial")) {
                    msg += String.Format("\"{0}\": {{ \"js\": \"base.js\", \"html\": \"{1}\", \"title\": \"Page Title\" }},\n", 
                        meta.Identity.Get("alias").Replace("Partial/", ""), meta.Identity.Get("alias"));
                }
            }

            return new HttpResponseMessage() { Content = new StringContent(msg) };
        }
    }

Then on the Layout.cshtml I put this in...

    HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://localhost:30320/MySite/api/BTH.Themis/ContentItemArray");
    req.Method = "GET";
    req.ContentType = "text/plain";
    string jsArray = String.Empty;

    try {
        using (HttpWebResponse res = (HttpWebResponse)req.GetResponse()) {
            jsArray = new StreamReader(res.GetResponseStream()).ReadToEnd();
        }
    } catch (Exception) { }

This at least gave me what I needed in order to render the array and it was done at render time.  I'm positive this is a real hack but it at least works until I find a more robust solution.

Coordinator
Nov 27, 2012 at 11:26 PM

You shouldn't be building JSON yourself, especially with String.Format. WebAPI knows perfectly well how to generate JSON.

Nov 28, 2012 at 1:13 AM

Even if not using WebAPI, you can build JSON nicely with anonymous objects, and with the System.Web.Script.Serialization.JavaScriptSerializer class, returning a JsonResult.

Out of curiosity, why are you building the app this way? Why do a web service request from the server, back to the server again? You could be passing a shape to the layout to render the JS. 

Nov 28, 2012 at 5:02 AM
Edited Nov 28, 2012 at 5:21 AM

I'm not trying to work with a JSON object is the thing. The code being generated from the first code snippet is kind of trying to bypass it returning a plain string that the Layout page is then rendering with a @(new HtmlString(jsArray)) statement.  I could probably just use the regular JSON return results and then work with that in the Layout.cshtml and render it but right now I'm purely trying to return a single string that would be rendered as a JS array to the user.  I'm more concerned with figuring out the guts of Orchard than the JSON object itself. 

You are exactly correct in that I should be passing a shape to the layout and rendering the JS.  This has been my problem all along and I'm ignorant and what I was trying to get at in my earlier posts.  This was my solution to give kind of an idea on what I'm attempting to accomplish... think of it as an example of what I want :) 

The passing of the shape is the answer I needed.  Again, very new to this as I know what I need but how to go about getting that is the problem and I have a limited amount of time to get this implemented.  Now I just need to figure out how to pass this shape to the layout and make this happen "properly" instead of this round robin BS I have created.

Nov 28, 2012 at 5:33 AM

As an update, I just looked at the documentation for shapes and it appears this was the missing element I needed.  Thank you so much for the assistance!

Nov 28, 2012 at 1:42 PM

Cool. Looking at what you did I would have just done it using JSON serialization because you can let the framework do the serialization for you, and JSON is valid javascript (well, almost always anyway). It's brittle to build the strings yourself because you might not properly escape some values, etc. 

I'm glad we were able to help you out. To render the data in the Layout, the specific method I would probably use is to push the shape to a specific zone of the Layout. I'm going to assume you want to render the javascript in a zone named "Footer". One approach would be to use a Filter to inject the shape at the "OnResultExecuting" event.

In your module, make a "Filters" folder, and create a BHillDynamicJsFilter.cs file there: 

 

namespace bhill.Filters {
    public class BHillDynamicJsFilter : FilterProvider, IResultFilter {

        private readonly IWorkContextAccessor _wca;
        dynamic _shapeFactory { get; set; }
        public IOrchardServices Services { get; set; }
        public Localizer T { get; set; }

        public MyEdgeFilter(IWorkContextAccessor wca, IShapeFactory shapeFactory, IOrchardServices orchardServices) {
            _wca = wca;
            _shapeFactory = shapeFactory;
            Services = orchardServices;
        }
        
        /// <summary>
        /// This is where we inject the BHillDynamicJsShape shape into the site's layout (in the "Footer" zone):
        /// </summary>
        /// <param name="filterContext"></param>
        public void OnResultExecuting(ResultExecutingContext filterContext) {
            // Should only run on a full view rendering result
            if (!(filterContext.Result is ViewResult)) {
                return;
            }

            var workContext = _wca.GetContext();
            var footerZone = workContext.Layout.Footer;
            
            footerZone.Add(
              _shapeFactory.BHillDynamicJsShape(
                Pages = Services.ContentManager.Query(VersionOptions.Published, "Page").List()
                , AboutPages = Services.ContentManager.Query(VersionOptions.Published, "AboutPage").List()
              )
            );
        }

        public void OnResultExecuted(ResultExecutedContext filterContext) {

        }
    }
}

 

In your module, create a file in the "Views" folder, whose name matches the shape you created the code above using the _shapeFactory object. That would be BHillDynamicJsShape.cshtml: 

 

@model dynamic;
@{
  IEnumerable<ContentItem> pages = Model.Pages;
  IEnumerable<ContentItem> aboutPages = Model.AboutPages;
  string jsArrayCSharp = "";
  // ... do some work here to build the jsArray
  jsArrayCSharp = "[" + jsArrayCSharp + "]";
}

<script type="text/javascript">
  $(document).ready(function() {
    // Init the javascript array object:
    var jsArray = @Html.Raw(jsArrayCSharp);
    
    // Do some stuff with the array...
    var myObj.Init(jsArray); 
  });
</script>

 

 

Nov 28, 2012 at 1:46 PM

Also, if you just want the javascript to render at the bottom of the page (just before the closing </body> tag), or in the <head>tag, you can do this: 

@using(Script.Foot()) {
<script type="text/javascript">
  $(document).ready(function() {
    // Init the javascript array object:
    var jsArray = @Html.Raw(jsArrayCSharp);
    
    // Do some stuff with the array...
    var myObj.Init(jsArray); 
  });
</script>
}
Use Script.Foot() or Script.Head() as desired, to get the <script> tag within the using block to render in the document head, or at the foot of the page. If you do this, then it doesn't matter which Zone you push the shape to, you can push it to any Zone since you aren't going to render anything in that shape to its containing Zone. 

Nov 28, 2012 at 5:29 PM

This is absolutely amazing and its all suddenly starting to make a lot more sense!  Thank you so much for taking the time to explain this to me.  I'm really trying to push my customers to .NET MVC away from Wordpress and so I've really been trying to dig into the guts of this CMS to understand how it all works.  I've got a firm grasp on MVC but this has added a bit of a learning curve however I'm digging through it :)  Every day it's getting a little easier with the help of others and folks such as yourselves.  I *really* appreciate the insight here!  Now to get rid of the coat hanger system I'm responsible for and build it properly...

Nov 30, 2012 at 1:46 AM
Edited Nov 30, 2012 at 1:48 AM

Just as an FYI to anyone looking to use this, the code:

            footerZone.Add(
              _shapeFactory.BHillDynamicJsShape(
                Pages = Services.ContentManager.Query(VersionOptions.Published, "Page").List()
                , AboutPages = Services.ContentManager.Query(VersionOptions.Published, "AboutPage").List()
              )

had a small typo on the =, should be

            footerZone.Add(
              _shapeFactory.BHillDynamicJsShape(
                Pages: Services.ContentManager.Query(VersionOptions.Published, "Page").List()
                , AboutPages: Services.ContentManager.Query(VersionOptions.Published, "AboutPage").List()
              )

Just for anyone else working with this.  This whole thing worked GREAT!!  Thank you so much for all the help.

Nov 30, 2012 at 1:54 AM

Just as a side question, the old hack code I came up with seemed to have a problem with the TransactionScope.  Not really sure why but it would come up randomly after it was executed.  I tried adding a using clause to isolate it into its own transaction scope but it then fired a nesting error.  Any insight as to why this would occur?  Just for future reference if I need to hit the WebAPI and use the database...

Mar 25, 2013 at 1:32 PM
using (var scope = new TransactionScope(TransactionScopeOption.Suppress))