I'm trying to implement a thought i had to allow user defined sections to be dynamically generated for my MVC 3 Razor site.
A template would look something like this
<div class="sidebar">
#RenderSection("Sidebar", false)
</div>
<div class="content">
#RenderSection("MainContent", false)
#RenderBody()
</div>
Adding a view with the following code gives me the result I would expect
DefineSection("MainContent", () =>
{
this.Write("Main Content");
});
DefineSection("Sidebar", () =>
{
this.Write("Test Content");
});
Output:
<div class="sidebar">Test Content </div>
<div class="content">Main Content <p>Rendered body from view</p></div>
Looking at this it seemed easy enough to create a model
Dictionary<SectionName, Dictionary<ControlName, Model>>
var sectionControls = new Dictionary<string, Dictionary<string, dynamic>>();
sectionControls.Add("MainContent", new Dictionary<string, dynamic>()
{
{"_shoppingCart", cart}
});
sectionControls.Add("Sidebar", new Dictionary<string, dynamic>()
{
{ "_headingImage", pageModel.HeadingImage },
{ "_sideNav", null }
});
pageModel.SectionControls = sectionControls;
So the above code declares two template sections ("MainContent" with a cart and a "Sidebar" with an image and a nav.
So now my view contains code to render the output like so
foreach(KeyValuePair<string,Dictionary<string,dynamic>> section in Model.SectionControls)
{
DefineSection(section.Key, () =>
{
foreach (KeyValuePair<string, dynamic> control in section.Value)
{
RenderPartialExtensions.RenderPartial(Html, control.Key, control.Value);
}
});
}
Now when I run this code, both sections contain the same content! Stepping through the code shows the load path is as follows
Action Returns, Code above runs in View, LayoutTemlpate begins to load. when RenderSection is called for these two sections in the layout template, the view Runs again! What seems even stranger to me is that the end result is that the "HeadingImage" and "SideNav" end up in both the Sidebar and MainContent sections. The MainContent section does not contain the cart, it contains a duplicate of the sidebar section.
<div class="sidebar">
<h2><img alt=" " src="..."></h2>
..nav..
</div>
<div class="content">
<h2><img alt=" " src="..."></h2>
..nav..
<p>Rendered body from view</p>
</div>
Commenting out one of the two section definitions in the Controller causes the other one to be the only item (but it is still duplicated!)
Has anyone had this issue before or know what limitation could be causing this behavior?
Edit: Excellent. Thanks for the linkage as well! I'm hurting for the new version of resharper with razor support.
Your lambda expressions are sharing the same section variable.
When either lambda is called, the current value of the variable is the last section.
You need to declare a separate variable inside the loop.
foreach(KeyValuePair<string,Dictionary<string,dynamic>> dontUse in Model.SectionControls)
{
var section = dontUse;
DefineSection(section.Key, () =>
{
foreach (KeyValuePair<string, dynamic> control in section.Value)
{
RenderPartialExtensions.RenderPartial(Html, control.Key, control.Value);
}
});
}
Related
I just started using umbraco 4 days ago and I'm stuck with this problem.
I have a page named "About Us" that is a child under "Home" and I need to get some of its properties in the "Home" Page.
I created a partial view macro to do this but I get the error loading partial view script instead.
Below is my code:
#{ var selection = CurrentPage.Children.Where(x => x.Name == "aboutus"); }
#if (selection.Any())
{
<ul>
#foreach (var item in selection)
{
<div class="sc-inner">
<h4>
#item.PageTitle</h4>
<p>
#Umbraco.Truncate(#item.mainAboutDescription, 100)</p>
<a class="btn btn-danger" href="#item.Url">Read More...</a>
</div>
break;
}
</ul>
}
Can someone please tell me what I'm doing wrong?
Thanks in advance.
Edits:
The error on the website I got is below;
Error loading Partial View script (file: ~/Views/MacroPartials/homePage_aboutUsSection.cshtml)
Assuming the partial is inheriting UmbracoTemplatePage the CurrentPage property is a dynamic, you cannot use lambda expressions as an argument to a dynamically dispatched operation.
If you wish to use Linq to query the content use Model.Content rather than CurrentPage which is an IPublishedContent e.g.
#{ var selection = Model.Content.Children.Where(x => x.Name == "aboutus"); }
Note: the Name property will return the documents Name as entered in the CMS most likely 'About Us' instead of 'aboutus'
Using the about will return an IEnumerable<IPublishedContent> so you will need to use the GetPropertyValue method rather than accessing the content dynamically:
#inherits Umbraco.Web.Mvc.UmbracoTemplatePage
#{ var selection = Model.Content.Children.Where(x => x.Name == "aboutus"); }
#if (selection.Any())
{
<ul>
#foreach (var item in selection)
{
<li class="sc-inner">
<h4>
#item.GetPropertyValue("pageTitle")
</h4>
<p>
#Umbraco.Truncate(#item.GetPropertyValue<string>("mainAboutDescription"), 100)
</p>
<a class="btn btn-danger" href="#item.Url">Read More...</a>
</li>
break;
}
</ul>
}
Very bad practice to use the Name property for querying in your code - what if somebody changes the Name to About for example. Your code would break. If there is something else that is unique on the node then it would be better.
For example if you have an aboutUs document type for your about us page you could use:
Model.Content.Children.Where(x => x.DocumentTypeAlias == "exactNameOfYourDocTypeAliasHere")
or, although not so robust, the Id of the page could be used.
Model.Content.Children.Where(x => x.Id == 1234)
The Id is far less likely to change than the Name of a key page like this.
I would generally not use the Id in code either but it is far better than Name
I have a web page that dynamically creates components by rendering them through a RenderPartial Html Helper like this
Html.RenderPartial("_UploadStaticField", config);
Within the config object is a field called 'isUnderReview'. When this is set to true the components should be disabled by using the code below
//Single selection
<div class="editor-section">
<div class="label">
#Html.DisplayEditLabel(Model.Label, Model.Required.Value)
</div>
<div class="field large-text-field">
#Html.DropDownListFor(m => m.SelectedListOptionID, new SelectList(Model.ListOptions, "ID", "Name", Model.SelectedListOptionID).OrderBy(l => l.Value), new Dictionary<string, object>{
{"id", "staticField_" + Model.ID}})
</div>
</div>
<script>
$(document).ready(function () {
if ("#Model.IsUnderReview" == "True") {
document.getElementById("staticField_" + "#Model.ID").disabled = true;
}
});
</script>
and..
//Multiple selection
<div class="editor-section">
<div class="label">
#Html.DisplayEditLabel(Model.Label, Model.Required.Value)
</div>
<div class="field large-text-field">
#Html.ListBoxFor(x => x.SelectedRoles, filetypes, new { #class = "multiselectFileTypes" , id = "staticFieldM_" + Model.ID})
</div>
</div>
#Scripts.Render(BundleConfig.Scripts_MultiSelect)
<script>
$(document).ready(function () {
if ("#Model.IsUnderReview" == "True") {
document.getElementById("staticFieldM_" + "#Model.ID").disabled = true;
}
});
</script>
The code works to the point that the methods run but the components are still able to be used. Is there a way of cancelling any users selections which will serve as disabling as the values wont change?
Scripts should never be in partials (you potentially generating multiple inline scripts that may cause other problems, especially with the bundle). However a script is not even necessary for this, you can use a simple if statement or conditional attributes to generate what you want.
When you say "but the components are still able to be used", I'm guessing that your using a plugin to generate controls as suggested by Scripts.Render(BundleConfig.Scripts_MultiSelect) which will be hiding the original <select> element and generating its own html which is why it still interactive.
But the next problem is that disabled controls do not post back a value, so the vales of SelectedListOptionID and SelectedRoles will be their default, possibly resulting in your app failing depending on the code in your POST method.
Move the #Scripts.Render() into you view or layout, delete the scripts to disable the element and then change the partial to
#if(Model.IsUnderReview)
{
#Html.HiddenFor(m => m.SelectedListOptionID) // if you want the value to be posted
// add an element to display the Name associated with the SelectedListOptionID
// if necessary, for example your view model might include a property
// string SelectedListOptionName
}
else
{
#Html.DropDownListFor(m => m.SelectedListOptionID, new SelectList(Model.ListOptions, "ID", "Name").OrderBy(l => l.Value))
}
Side notes:
There is no reason to add you own id attribute (the
DropDownListFor() method generates <select
id="SelectedListOptionID" ... >
Remove the last parameter of the SelectList constructor
(Model.SelectedListOptionID) - its ignored by the
DropDownListFor() method. I also recommend your model contains an IEnumerable<SelectListItem> OptionsList property and you populate that in the controller so that it can simply be #Html.DropDownListFor(m => m.SelectedListOptionID, Model.OptionsList)
I'm wondering is it possible to have a partial view display in a tooltip. I'm trying to design a page that has a group of students and when I hover over a student some details appear about them, a list of their subjects and some other details. I know I can put the items in the title, but I can't get all the info I need from there, and my details view has the info I need. Thanks in advance
Here's my current code
IEnumerable<StudentApp.Models.Student>
#foreach (var item in Model)
{ <div class="col-md-2 col-sm-4">
<img title="#item.Name, #item.StudentNo " class="img img-responsive" src="#item.StudentPic" />
</div>
}
#section scripts
{
<script type="text/javascript">
$(document).ready(function () {
$(document).tooltip();
});
</script>
}
Here's my details view
<div class="display-label">
#Html.DisplayNameFor(model => model.Name)
</div>
<div class="display-field">
#Html.DisplayFor(model => model.Name)
</div>
<div class="display-label">
#Html.DisplayNameFor(model => model.StudentNo)
</div>
<div class="display-field">
#Html.DisplayFor(model => model.StudentNo)
</div>
#foreach (var item in Model.Students)
{
<li>#item.Course</li>
}
You can use the content option to make the tooltip's contents whatever you need them to be. The MVC code could be placed anywhere in your page, so long as you can use jQuery to grab it (eg. in a noscript tag or some such):
$(document).tooltip({
items: ".myTooltipClass",
content: function() {
// .text() will unescape any contained HTML and render it properly;
// use .html() if your content doesn't contain additional HTML
return $("noscript#myMvcContent").text();
}
});
Here's a fiddle demo: http://jsfiddle.net/guke50uw/
As you can use $(this) inside the content function, you can make use of that with HTML data attributes to return different tooltips for the different items; depends on how your page is structured.
I would use the tooltip's open method and set the content of the tooltip object via AJAX there:
$(document).ready(function () {
$(document).tooltip({ open: function(event, ui) {
$(ui.content).load("[url for returning view contents]");
}
});
you may need to use the instance property of the tooltip or maybe via the ui parameter of the open function to retrieve your student number or other identifier to properly query your partial view, but this should help get you going.
jsFiddle at http://jsfiddle.net/nAgfQ/2/ (See top of HTML section for explanation and workaround.)
Scenario
I'm using jQuery Mobile (1.4.2) and KnockoutJS (3.1.0) to build a very straightforward single-page tab-based web app for displaying data to business users.
Code
Here's the JS:
$(function () {
var Tab = function (Title, TabID) {
var self = this;
self.Title = ko.observable(Title);
self.TabID = ko.observable(TabID);
self.TabHref = ko.computed(function () {
return '#' + self.TabID();
});
};
function DashboardViewModel() {
var self = this;
self.Title = ko.observable();
self.DashboardID = ko.observable();
self.tabs = ko.observableArray([
new Tab("Tab 1", "tabs-1", []),
new Tab("Tab 2", "tabs-2", [])]);
self.refreshTabs = function () {
$('#tabs').tabs("refresh").tabs("option", "active", 0);
//Added to callback to convert navbar div into jQuery Mobile Navbar
$('#dashboard_navbar').navbar();
};
}
dvm = new DashboardViewModel();
ko.applyBindings(dvm);
});
Here's the body content of the page:
<body>
<div data-role="page" id="page-1">
<div data-role="header">
<h1>jQuery Mobile Tabs Test</h1>
</div>
<div data-role="content">
<div data-role="tabs" id="tabs">
<div data-role="navbar" id="dashboard_navbar">
<ul data-bind="template { foreach : tabs }">
<li><a data-bind="attr : { href: TabHref } , text: Title" data-ajax="false"></a>
</li>
</ul>
</div>
<div data-bind=" template { foreach :tabs, afterRender: refreshTabs}">
<div data-bind="attr : { id: TabID }" class="ui-body-d ui-content">
<h4 data-bind="text: Title" />
</div>
</div>
</div>
</div>
</div></body>
Issue
When you have a Tab widget in jQuery Mobile, you are encouraged to declare an element to have a data-role attribute set to "navbar."
When jQuery renders the page, it looks for the first ul child element of the selected element, and reads the number of li elements underneath that ul.
It then uses this to add a class with the naming schema ul-grid-N, where N is the letter of the alphabet corresponding to the number of elements found minus 1 (i.e. ul-grid-a for 2 elements, ul-grid-b for 3, etc.) If there is only one element, it uses a special class ul-grid-solo.
However, when you use KnockoutJS to load a set of bound tabs, you just supply a single li element as a template underneath a foreach binding. jQuery Mobile only sees the 1 element and so adds the ul-grid-solo class and then the navbar li elements end up being rendered as stacked on top of one another instead of horizontally aligned.
Workaround
The solution I have so far is to remove the "navbar" data-role and instead use KnockoutJS's afterRender callback to convert the element into a navbar once all the bound tabs have been inserted. (See the *refreshTab*s function in the DashboardViewModel object.)
This works, but is less than ideal since it forces the ViewModel to know something about the View which is an MVVM no-no.
Questions
Can I tell jQuery Mobile to hold off applying the grid class to the navbar until after the bindings have been applied? I poked around its API but didn't see anything particularly useful.
Is there something I can do with Knockout's custom bindings? Again, trying not to inject any DOM manipulation into the ViewModel.
In general, any other workarounds, comments on the code, etc. would be appreciated.
Working with knockout and jQuery Mobile for a while, I can confirm that they simply do not play nice together. Our team has a list of re-usable knockout custom bindings just for working with jQuery mobile, because they're such a pain.
You could essentially wrap up the below workaround, or your own, into a custom binding that you'd use in place of foreach. Or subscribe to changes to the array of navbar items and update there.
Workaround based on your jsFiddle, trying to recreate the navbar, you have to also rip out the dynamic markup that jQuery mobile puts into the elements. Try adding this (source):
navbar.find("*").andSelf().each(function(){
$(this).removeClass(function(i, cn){
var matches = cn.match (/ui-[\w\-]+/g) || [];
return (matches.join (' '));
});
if ($(this).attr("class") == "") {
$(this).removeAttr("class");
}
});
JSFiddle
I'm new to ASP.NET MVC SPA and Knockout.js os maybe it's a simple mistake I made...
Situation: I have two partialviews in my website and I want that every partialview has his own Knockout ViewModel so I won't get a huge ViewModel.
My current ViewModel:
/// <reference path="../_references.js" />
function MobileDeliveriesViewModel() {
var self = this;
// Data
self.currentDelivery = ko.observable();
self.nav = new NavHistory({
params: { view: 'deliveries', deliveryId: null }
});
// Test
self.foo = "FooBar"
self.bar = "BarFoo"
self.nav.initialize({ linkToUrl: true });
// Navigate Operations
self.showDeliveries = function () { self.nav.navigate({ view: 'deliveries' }) }
self.showCustomers = function () { self.nav.navigate({ view: 'customers' }) }
}
function BarFooViewModel() {
var self = this
//MobileDeliveriesViewModel.call(self)
self.bar2 = "BarFooTwo"
}
ko.applyBindings(new MobileDeliveriesViewModel());
ko.applyBindings(new MobileDeliveriesViewModel(), $('#BarFoo')[0]);
ko.applyBindings(new BarFooViewModel(), document.getElementById('BarFoo'));
My Index.cshtml:
<div data-bind="if: nav.params().view == 'deliveries'">
#Html.Partial("_DeliveriesList")
</div>
<div class="BarFoo" data-bind="if: nav.params().view == 'customers'">
#Html.Partial("_CustomersList")
</div>
<script src="~/Scripts/App/DeliveriesViewModel.js" type="text/javascript"></script>
My CustomerPartialView:
<div id="BarFoo" class="content">
<p data-bind="text: bar"></p>
<p data-bind="text: bar2"></p>
<button data-bind="click: showDeliveries, css: { active: nav.params().view == 'deliveries' }">Deliveries</button>
</div>
My DeliveriesPartialView:
<div class="content">
<p data-bind="text: foo"></p>
<button data-bind="click: showCustomers, css: { active: nav.params().view == 'customers' }">Customers</button>
</div>
If I run this, it won't recognize the bar2 propertie in my BarFooViewModel...
I have tried 2 different applyBindings at the end of my ViewModel.
Anybody got an idea or is their a better way/pattern to do this?
are there JS errors on page?
nav.params().view
but params: { view: 'deliveries', deliveryId: null } - it's not function.
and if you want use a few view models on single page - check this http://www.knockmeout.net/2012/05/quick-tip-skip-binding.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+KnockMeOut+%28Knock+Me+Out%29 acticle. you have to use "stopBinding"
It looks like you are applying multiple data bindings to the same sections.
ko.applyBindings(new MobileDeliveriesViewModel();
This will bind to all elements one the page.
ko.applyBindings(new MobileDeliveriesViewModel(), $('#BarFoo')[0]);
this will try to bind to all elements inside the div
ko.applyBindings(new BarFooViewModel(), document.getElementById('BarFoo'));
This will also try to bind to all elements inside the div.
To keep things simple, you should try to bind a single view model to a single html section. I've found that trying to bind two view models in the same html section has been hard to get work correctly and trouble shoot.
Jack128's answer also makes some good points.