Feeds:
Posts
Comments

If you create a custom module that has embedded javascript (for example a field control), the fact that the javascript gets bundled into the module for deployment is great for portability.

It can be a little bit tedious, however, for troubleshooting, editing, and debugging. I make use of the Chrome developer tools extensively, for example, to set breakpoints in my javascript, and inspect properties. Even just locating my embedded scripts within the many “Telerik.Web.UI.WebResource.axd” files can be a challenge. And when I want to incorporate changes, I have to rebuild my module, and recycle the website, waiting for the changes to come through on the browser.

I have come up with a (hopefully) better way to work through these changes: during development I now have my module js file call and load an external, static js file that can live in the root of the Sitefinity webapp and which can be edited and reloaded on the fly. Then, when edits are all complete and everything is good, I can move the external js back into the module for bundling.

For example, I have a custom “DocumentSelectorFieldControl” that I’m building up in a custom module:

1

In the “DocumentSelectorFieldControl.js” file, I have the following:

function loadScript(url, callback) {

    var script = document.createElement("script");
    script.type = "text/javascript";

    if (script.readyState) {  //IE
        script.onreadystatechange = function () {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                callback();
            }
        };
    } else {  //Others
        script.onload = function () {
            callback();
        };
    }

    script.src = url;
    document.getElementsByTagName("head")[0].appendChild(script);
};

// temporarily working from static js file in sitefinity web app folder
// move back here when working
loadScript("/testing.js", function () {
    //initialization code
});

Type.registerNamespace("MyApp.CustomFields.DocumentSelector");

*NOTE* that you must include the “registerNamespace” method call here (it’s my last line). If it’s in the external js file (testing.js) it doesn’t function. Presumably this is because of a timing issue where your module is trying to access the js object via namespace, but it’s null.

I then add a “testing.js” file to my SitefinityWebApp:

1

And now, in testing.js in my SitefinityWebApp, I can put all of my custom code for the field control:

1

Advertisements

The documentation for Sitefinity is a bit… hit or miss. Some of the newer features, such as the Fluent API, simply do not have enough examples / documentation available to be able to work out of the gate. Such was the case with my task for today: adding a custom module’s widgets into the Siteifinty install’s toolbox.

First of all, create your Resource class. I created this a the root of my project, in the same folder as the ContentModuleBase class. This class will contain, among other things, all of the friendly names and descriptions that your widget will use:

using Telerik.Sitefinity.Localization;
using Telerik.Sitefinity.Localization.Data;

namespace me.Sitefinity.BusinessOpportunities
{
    /// <summary>
    /// Resource class for the procuts module
    /// </summary>
    [ObjectInfo(typeof(BusinessOpportunitiesResources), Title = "BusinessOpportunitiesResourcesTitle", Description = "BusinessOpportunitiesResourcesDescription")]
    public class BusinessOpportunitiesResources : Resource
    {
                #region Constructors
        
        /// <summary>
        /// Initializes new instance of  class with the default .
        /// </summary>
        public BusinessOpportunitiesResources()
        {
        }

        /// <summary>
        /// Initializes new instance of  class with the provided .
        /// </summary>
        /// 
        public BusinessOpportunitiesResources(ResourceDataProvider dataProvider)
            : base(dataProvider)
        {
        }

        #endregion

        #region Class Description

        /// <summary>
        /// The title of this class
        /// </summary>
        [ResourceEntry("BusinessOpportunitiesResourcesTitle",
            Value = "Business Opportunities",
            Description = "The title of this class.",
            LastModified = "2012/12/07")]
        public string BusinessOpportunitiesResourcesTitle
        {
            get { return this["BusinessOpportunitiesResourcesTitle"]; }
        }

        /// <summary>
        /// The description of this class
        /// </summary>
        [ResourceEntry("BusinessOpportunitiesResourcesDescription",
            Value = "Contains localizable resources for Business Opportunities module.",
            Description = "The description of this class.",
            LastModified = "2012/12/07")]
        public string BusinessOpportunitiesResourcesDescription
        {
            get { return this["BusinessOpportunitiesResourcesDescription"]; }
        }

        #endregion

        #region toolbox resources

        [ResourceEntry("BusinessOpportunitiesToolboxTitle",
            Value = "Bidness",
            Description = "Title of the toolbox entry",
            LastModified = "2012/12/07")]
        public string BusinessOpportunitiesToolboxTitle
        {
            get { return this["BusinessOpportunitiesToolboxTitle"]; }
        }

        [ResourceEntry("BusinessOpportunitiesToolboxDescription",
            Value = "Contains localizable resources for Business Opportunities module.",
            Description = "Description of the toolbox entry",
            LastModified = "2012/12/07")]
        public string BusinessOpportunitiesToolboxDescription
        {
            get { return this["BusinessOpportunitiesToolboxDescription"]; }
        }

        #endregion toolbox resources

        #region widget resources

        /// <summary>
        /// phrase: Widget that displays real estate items
        /// </summary>
        [ResourceEntry("MasterListViewTitle",
            Value = "Opportunities",
            Description = "phrase: Widget that displays real estate items",
            LastModified = "2012/12/07")]
        public string MasterListViewTitle
        {
            get { return this["MasterListViewTitle"]; }
        }

        
        /// <summary>
        /// phrase: Widget that displays real estate items
        /// </summary>
        [ResourceEntry("MasterListViewDescription",
            Value = "Widget that displays real estate items",
            Description = "phrase: Widget that displays real estate items",
            LastModified = "2012/12/07")]
        public string MasterListViewDescription
        {
            get { return this["MasterListViewDescription"]; }
        }

        #endregion widget resources
    }
}

Now you will be able to reference these friendly strings when you integrate your widget into the toolbox.

Next you have to make sure that this resources file is installed into Sitefinity by your custom module. In your ContentModuleBase class, in your Initialize method, make sure to add it in as follows:

public override void Initialize(ModuleSettings settings)
{
	base.Initialize(settings);

	// initialize configuration file
	App.WorkWith()
		.Module(settings.Name)
		.Initialize()
			.Configuration<BusinessOpportunitiesConfig>()
			.Localization<BusinessOpportunitiesResources>()
			.WebService<BusinessOpportunitiesBackendService>("Sitefinity/Services/Content/BusinessOpportunities.svc");
}

The key line is the .Localization. This is the same as:

Res.RegisterResource();

in the pre-fluent API implementation.

And then finally, in the InstallConfiguration method, you need to add your section and widgets, ensuring that you reference the localization settings (“Resources”) along the way:

/// <summary>
/// Installs module's toolbox configuration.
/// </summary>
/// <param name="initializer">The initializer.</param>
protected override void InstallConfiguration(SiteInitializer initializer)
{
	// Module widget is installed on Bootstrapper_Initialized
	initializer.Installer
		.PageToolbox()
		
			.LoadOrAddSection("MySection2")
				.LocalizeUsing<BusinessOpportunitiesResources>()
				.SetTitle("BusinessOpportunitiesToolboxTitle")
				.SetDescription("BusinessOpportunitiesToolboxDescription")

				.LoadOrAddWidget<MasterListView>("MasterListView")
					.LocalizeUsing<BusinessOpportunitiesResources>()
					.SetTitle("MasterListViewTitle")
					.SetDescription("MasterListViewDescription")
					.Done()

				.Done()
			.Done();
}

With that, you should end up with entries in your toolbox like the following:

Custom widget in Sitefinity Toolbox

Custom widget in Sitefinity Toolbox

In a recent project, one of my models had the following validation requirements:
If “CompletedDate” has a value (a date value), “Status” must be set to “Completed”. If “Status” is set to “Completed”, the “CompletedDate” must be set.

In other words, validation wasn’t simply that a value must be a string, or a number between x and y, etc; the validation of one field is dependant on the value in the other and vice versa.

I first went down this route, running the validation in the data layer (in the Entity Framework stuff):
MSDN: How to: Customize Data Field Validation in the Data Model

This was pretty good, but the problem is *when* the validation occurs when using this approach: when the model is being saved. I want validation to run *before* saving the data, so that MVC can know that there’s a problem, and send some friendly message to the user. A typical “edit” command in a controller looks like this:

[HttpPost]
public ActionResult Edit(ProjectTask projecttask)
{
	if (ModelState.IsValid)
	{
		db.ProjectTasks.Attach(projecttask);
		db.ObjectStateManager.ChangeObjectState(projecttask, EntityState.Modified);
		db.SaveChanges();
		return RedirectToAction("Details", "Project", new { id = projecttask.Workorder.CallID });
	}
	return View(projecttask);
}

So I *do* have an option, I could put a try around db.SaveChanges, and catch my validation error there. However, I would then have to grab the error message and figure out some way to send that back to the client (via ViewBag, most likely). I didn’t like this.

I then inspected this ModelState.IsValid method, and had the thought to create an extension method along the lines of ModelState.IsValidProjectTask(). With this, I can add error messages into the ModelState, specific to the fields. This means Razor script like:

@Html.ValidationMessageFor(model => model.CompletionDate)

will work, returning a proper error message where appropriate.

So here’s what I did:

[HttpPost]
public ActionResult Edit(ProjectTask projecttask)
{
	if (ModelState.IsValidProjectTask())
	{
		db.ProjectTasks.Attach(projecttask);
		db.ObjectStateManager.ChangeObjectState(projecttask, EntityState.Modified);
		db.SaveChanges();
		return RedirectToAction("Details", "Project", new { id = projecttask.Workorder.CallID });
	}
	return View(projecttask);
}

and then in my Models namespace:

using System.Web.Mvc;

namespace MyProject.Models
{
    public static class ModelStateDictionaryExtensions
    {
        public static bool IsValidProjectTask(this ModelStateDictionary modelStateDictionary)
        {
            return modelStateDictionary.IsValid & ValidateDates(modelStateDictionary);
        }

        private static bool ValidateDates(ModelStateDictionary modelStateDictionary)
        {
            ModelState outState;
            var submittedDate = string.Empty;
            var submittedStatus = string.Empty;

            if (modelStateDictionary.ContainsKey("CompletionDate"))
            {
                modelStateDictionary.TryGetValue("CompletionDate", out outState);
                submittedDate = outState.Value.AttemptedValue;
            }
            if (modelStateDictionary.ContainsKey("Status"))
            {
                modelStateDictionary.TryGetValue("Status", out outState);
                submittedStatus = outState.Value.AttemptedValue;
            }

            if (submittedStatus.ToUpper() == "COMPLETED" & string.IsNullOrEmpty(submittedDate))
            {
                modelStateDictionary.AddModelError("CompletionDate", "Task completed, but no completion date provided.");
                return false;
            }
            if (submittedStatus.ToUpper() != "COMPLETED" & !string.IsNullOrEmpty(submittedDate))
            {
                modelStateDictionary.AddModelError("Status", "Completion date provided, but task not completed.");
                return false;
            }

            return true;
        }
    }
}

In a current  project, I need to pass an array of locations in to my Google map, and have the API find the directions between them. Google Maps API v.3 limits each directions request to a start point, an end point, and 8 waypoints in between. I need to be able to route more than this. The solution, essentially, is to break my list of stops down into groups, make multiple requests to Google, and stitch the results together when they come back.

This was (of course) a bit more easily said than done. For one thing, you cannot simply split your array into groups of 10, because you need to include the last stop of one array as the first stop of the subsequent array, otherwise there will be gaps in your route. That is, given an array of stops like this (and assuming the Google API max is 4, not 10 to keep the numbers lower):

[a,b,c,d,e,f,g,h]

I need a result like this:

[[a,b,c,d],[d,e,f,g],[g,h]]

So I wrote the following to create a multidimensional array of waypoints, that I can later iterate through and make requests on:

var batches = [];
var itemsPerBatch = 10; // google API max - 1 start, 1 stop, and 8 waypoints
var itemsCounter = 0;
var wayptsExist = stops.length &amp;gt; 0;

while (wayptsExist) {
	var subBatch = [];
	var subitemsCounter = 0;

	for (var j = itemsCounter; j &amp;lt; stops.length; j++) {
		subitemsCounter++;
		subBatch.push({
			location: new window.google.maps.LatLng(stops[j].Geometry.Latitude, stops[j].Geometry.Longitude),
			stopover: true
		});
		if (subitemsCounter == itemsPerBatch)
			break;
	}

	itemsCounter += subitemsCounter;
	batches.push(subBatch);
	wayptsExist = itemsCounter &amp;lt; stops.length;
	// If it runs again there are still points. Minus 1 before continuing to
	// start up with end of previous tour leg
	itemsCounter--;
}

In this sample, my ‘stops’ object is a rich object array, but as long as it contains your stops’ latitudes and longitudes, you’re set. Just update the location property above to match *your* stops object.

So at the end of this, we should have a two-dimensional array with a list of a list of waypoints. In the following method, we pass in this newly created multi-dimensional array (batches), a google.maps.DirectionsRenderer object (directionsDisplay), and a google.maps.DirectionsService object (directionsService). These latter two I have newed up in the calling code, and just send along references.

calcRoute: function (batches, directionsService, directionsDisplay) {
	var combinedResults;
	var directionsResultsReturned = 0;

	for (var k = 0; k &amp;lt; batches.length; k++) {
		var lastIndex = batches[k].length - 1;
		var start = batches[k][0].location;
		var end = batches[k][lastIndex].location;

		// trim first and last entry from array
		var waypts = [];
		waypts = batches[k];
		waypts.splice(0, 1);
		waypts.splice(waypts.length - 1, 1);

		var request = {
			origin: start,
			destination: end,
			waypoints: waypts,
			travelMode: window.google.maps.TravelMode.DRIVING
		};
		directionsService.route(request, function (result, status) {
			if (status == window.google.maps.DirectionsStatus.OK) {
				if (directionsResultsReturned == 0) { // first bunch of results in. new up the combinedResults object
					combinedResults = result;
					directionsResultsReturned++;
				}
				else {
					// only building up legs, overview_path, and bounds in my consolidated object. This is not a complete
					// directionResults object, but enough to draw a path on the map, which is all I need
					combinedResults.routes[0].legs = combinedResults.routes[0].legs.concat(result.routes[0].legs);
					combinedResults.routes[0].overview_path = combinedResults.routes[0].overview_path.concat(result.routes[0].overview_path);

					combinedResults.routes[0].bounds = combinedResults.routes[0].bounds.extend(result.routes[0].bounds.getNorthEast());
					combinedResults.routes[0].bounds = combinedResults.routes[0].bounds.extend(result.routes[0].bounds.getSouthWest());
					directionsResultsReturned++;
				}
				if (directionsResultsReturned == batches.length) // we've received all the results. put to map
					directionsDisplay.setDirections(combinedResults);
			}
		});
	}
}

When you make a route request on the directionsService object, Google returns with a DirectionsResult object that contains a DirectionsRoute object in it (see the API documentation at: http://code.google.com/apis/maps/documentation/javascript/reference.html#DirectionsResult).

This route object is fairly rich, and contains a copyright notice, the bounds of the route (to update the bounds of the map), the actual path of the route, and more. In my case, I only need to display the path, and have the bounds updated (to show the entire route, not just one batch), so I created a results object, put the first set of results into it, and then manually update the properties of the object as the subsequent responses come back.

I then use this combinedResults object to render the route.

One potential issue with this that I haven’t accounted for: what happens if / when the results come back in the wrong order? I think your route will get messed up. A better approach might be to stuff the results into another array (or dictionary) and then build the combined object once all the results are back (?).

Of course comments, improvements, etc welcome.

UPDATE 29/Feb/2012

I have updated my code, and modified it so that is should now return the final list of results in the correct order. Essentially I did as I suggested above: I store the results in an intermediary object with an index value and then, when I know I have them all, sort them into their correct order and build up a new object from that. This new object is the one I use to draw the route.

The modified calcRoute function is below.

calcRoute : function (batches, directionsService, directionsDisplay) {
	var combinedResults;
	var unsortedResults = [{}]; // to hold the counter and the results themselves as they come back, to later sort
	var directionsResultsReturned = 0;

	for (var k = 0; k &amp;lt; batches.length; k++) {
		var lastIndex = batches[k].length - 1;
		var start = batches[k][0].location;
		var end = batches[k][lastIndex].location;

		// trim first and last entry from array
		var waypts = [];
		waypts = batches[k];
		waypts.splice(0, 1);
		waypts.splice(waypts.length - 1, 1);

		var request = {
			origin : start,
			destination : end,
			waypoints : waypts,
			travelMode : window.google.maps.TravelMode.WALKING
		};
		(function (kk) {
			directionsService.route(request, function (result, status) {
				if (status == window.google.maps.DirectionsStatus.OK) {

					var unsortedResult = {
						order : kk,
						result : result
					};
					unsortedResults.push(unsortedResult);

					directionsResultsReturned++;

					if (directionsResultsReturned == batches.length) // we've received all the results. put to map
					{
						// sort the returned values into their correct order
						unsortedResults.sort(function (a, b) {
							return parseFloat(a.order) - parseFloat(b.order);
						});
						var count = 0;
						for (var key in unsortedResults) {
							if (unsortedResults[key].result != null) {
								if (unsortedResults.hasOwnProperty(key)) {
									if (count == 0) // first results. new up the combinedResults object
										combinedResults = unsortedResults[key].result;
									else {
										// only building up legs, overview_path, and bounds in my consolidated object. This is not a complete
										// directionResults object, but enough to draw a path on the map, which is all I need
										combinedResults.routes[0].legs = combinedResults.routes[0].legs.concat(unsortedResults[key].result.routes[0].legs);
										combinedResults.routes[0].overview_path = combinedResults.routes[0].overview_path.concat(unsortedResults[key].result.routes[0].overview_path);

										combinedResults.routes[0].bounds = combinedResults.routes[0].bounds.extend(unsortedResults[key].result.routes[0].bounds.getNorthEast());
										combinedResults.routes[0].bounds = combinedResults.routes[0].bounds.extend(unsortedResults[key].result.routes[0].bounds.getSouthWest());
									}
									count++;
								}
							}
						}
						directionsDisplay.setDirections(combinedResults);
					}
				}
			});
		})(k);
	}
}

UPDATE 25/Oct/2012

I put together a simple jsFiddle illustrating the code:
http://jsfiddle.net/ZyHnk/

UPDATE 29/Dec/2015

Levillain asked how he/she could display multiple routes. The short answer is to change the ‘stops’ object into a multidimensional array (a collection of stops arrays), and then loop through each set of stops and process the route. You must make sure and new up a new DirectionsRenderer for each request; if you reuse the same one, each query will wipe out the previous one.

I am unable to get the map bounds to update correctly. If anyone knows how to do this, I’m all ears.

Also, I threw this together very quickly, I’m sure it can be optimized / refactored.

Sample jsfiddle with multiple routes can be found here.

UPDATE 4/May/2016

Cesar Ulises Martinez Garcia in his comment below (May 3, 2016) has a workable solution to the 10 batch / OVER_QUERY_LIMIT problem. Essentially, when he hits an OVER_QUERY_LIMIT error, he pauses before sending another request. Thanks, Cesar!

 

As part of a Sharepoint solution, I had the need to remove the default Quick Launch entries (Lists, Libraries, Discussions), and insert my own sections with links to specific lists and pages.

The following method gets it done:

public static void CreateSideNav(string siteName, string webName)
{
	using (var spSite = new SPSite(siteName))
	{
		using (SPWeb spWeb = spSite.OpenWeb(webName))
		{
			try
			{
				SPNavigationNodeCollection leftNav = spWeb.Navigation.QuickLaunch;
				var nodesToDelete = new List<int>();

				for (int i = 0; i < leftNav.Count; i++)
				{
					if (leftNav[i] != null)
					{
						SPNavigationNode leftNavNode = leftNav[i];
						if (DefaultQuickLaunchEntriesToDelete.Contains(leftNavNode.Title))
							nodesToDelete.Add(i);
					}
				}

				nodesToDelete.Sort();
				nodesToDelete.Reverse();
				nodesToDelete.ForEach(x => leftNav[x].Delete());

				spWeb.Update();

				leftNav = spWeb.Navigation.QuickLaunch;

				// Add section headings
				var reportsNode = new SPNavigationNode("Reports", spWeb.Url, true);
				leftNav.AddAsFirst(reportsNode);

				var dataListsNode = new SPNavigationNode("Data Lists", spWeb.Url, true);
				leftNav.AddAsFirst(dataListsNode);

				var mainListsNode = new SPNavigationNode("Primary Lists", spWeb.Url, true);
				leftNav.AddAsFirst(mainListsNode);


				// Add individual links
				SPNavigationNode node;

				// MainListsNode
				node = new SPNavigationNode("Committees", string.Format("{0}/Lists/Committees", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Members", string.Format("{0}/Lists/Members", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Services", string.Format("{0}/Lists/Services", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				// DataListsNode
				node = new SPNavigationNode("Committee Types", string.Format("{0}/Lists/CommitteeTypes", spWeb.Url), true);
				dataListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Jurisdictions", string.Format("{0}/Lists/Jurisdictions", spWeb.Url), true);
				dataListsNode.Children.AddAsLast(node);

				spWeb.Update();
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.ToString());
			}
		}
	}
}

I then reference this from a feature receiver in my package, and pass in the specific site and web names. I also define a List of Quick Launch entries to remove like so:

static List<string> DefaultQuickLaunchEntriesToDelete { get { return new List<string>() { "Lists", "Libraries", "Discussions" }; } }

With this we will, from the specified web, remove those default entries (of course you can specify any you wish) and insert our own.

I really don’t like having to work with the ClientId of page elements in ASP.Net. You know, you give a textbox an ID of, say, “myTextbox” and then try to get a reference to it client side in Javascript and it fails. You try (with jQuery, say) to run:

$('#myTextbox').attr('value', 'BLAH!')

and it returns null. You inspect your generated html and find that your textbox has an ID of something like: ctl00_m_g_3bd415ee_814a_46e1_a0f1_a2cf8227bea0_ctl00_myTextbox

I understand why Microsoft molests the IDs that I assign to it – it’s making sure that (as dictated by W3c) IDs of elements on my page are unique, and it does this by assigning a GUID to my webpart, and then appending to it with the details and IDs of my individual controls.

But I want to (relatively easily) get a reference to the webpart GUID so that, in my webparts, I can manipulate my DOM elements client side (in Javascript).

So what I do (glad to hear if there are better approaches!) is:

1. Create and Implement an Interface to Expose the ClientId

I create an interface for my Visual Web Parts called IVisualWebPart that my User Controls implement. It looks (so far) like:

using System.Web.UI.WebControls.WebParts;

namespace MyNamespace.Interfaces
{
public interface IVisualWebPart
  {
    WebPartManager WebPartManager { get; set; }
    string ClientId { get; set; }
  }
}

I don’t actually use the WebPartManager in this example, but I use it in other cases, so I’ve just left it in here.

And then in the usercontrol itself:

#region IVisualWebPart members

WebPartManager webPartManager { get; set; }
public WebPartManager WebPartManager
{
get
{
if (webPartManager == null)
throw new NullReferenceException("The instantiating web part must assign WebPartManager to this UserControl.");

return webPartManager;
}
set
{
webPartManager = value;
}
}

string clientId { get; set; }
public string ClientId
{
get
{
if (clientId == null)
throw new NullReferenceException("The instantiating web part must assign ClientId to this UserControl.");

return clientId;
}
set
{
clientId = value;
}
}

#endregion

And in the WebPart code (when you create a Visual WebPart in Sharepoint, it generates a WebPart as well as a UserControl with code behind, and sets the WebPart to fetch and display the UserControl):

protected override void CreateChildControls()
{
Control control = Page.LoadControl(_ascxPath);
(control as IVisualWebPart).WebPartManager = this.WebPartManager;
(control as IVisualWebPart).ClientId = this.ClientID;
Controls.Add(control);
}

2. Expose the ClientId via Code

I can then expose the ID on the page by injecting some JS onto the page from code behind like this:

private void AddClientIdJs()
{
StringBuilder js = new StringBuilder();
js.Append("<script type='text/javascript'>");
js.Append("$(function () {");
js.Append("var thisClientId = \"" + this.clientId + "\";");
js.Append("DispForm_Committee_RelatedAppointmentsUserControl_startUp(thisClientId);");
js.Append("});");
js.Append("</script>");
this.Page.Header.Controls.Add(new LiteralControl(js.ToString()));
}

I call this method from an overridden “CreateChildControls()”.

You’ll note that I’m actually using the Client ID in a call to *another* javascript file (myUserControl_startUp(thisClientId)). I could just as easily assign the ClientId to a variable and use it on the page, but I like to instead keep my Javascript external to the page as much as possible and instantiate it this way. It lets me more easily reuse functions across pages, and just feels like a cleaner separation of concerns.

3. Reference your elements

And finally, in that external JS, you can now get at your elements with jQuery and via code like the following:

function myUserControl_startUp(thisClientId) {
  //alert('thisClientId: ' + thisClientId);
  var selectedTabIdx = $('#' + thisClientId + '_ctl00_hiddenTabIdx').val();
}

Was making good progress with provisioning a site (with lists, content types, lookups, etc) in Sharepoint 2010, and thought I had the structural stuff licked. Moved on to the forms (dispform etc) and quickly found that I needed to include Dependant Lookups along with my Lookup Fields. This proved to be a bit tricky for me.

Dependant Lookups are new in Sharepoint 2010 and allow you to return virtual columns along with your Lookup Columns. They reveal themselves in the UI as checkboxes in the settings for Lookup Fields, and create additional columns in the format TargetList:SubColumn. So lets say I’ve got 2 lists: Customers and Orders that represent a one:many relationship. In Orders, I’ll need a Lookup Field (likely called “Customer”) that is a lookup to the Customers list.

On top of this, to make a parent-child form display correctly in the dispForm for Customers (ie a dispForm for a customer that shows all of their orders), I need to also reveal the ID of the Customer in the lookup field in Orders. So when I look at my content type for the Orders list, there will be an entry “Customer” of type lookup, as well as as entry “Customer:ID” of type lookup.

So I wrote the following method to create the lookup, add any additional dependant lookups desired, and then return the lookup field to the calling method (so it can then add it to the content type, etc).

        /// <summary>
        /// Create a site column, at the specified web level, of type lookup and wire it up to the appropriate list and column.
        /// This creates the field, which will then be ready to be added to a content type and applied to a list.
        /// Note this must be done AFTER referenced list instances have been initiated.
        /// </summary>
        public static SPFieldLookup CreateLookupField(string fieldDisplayName, string fieldStaticName, string group, bool required, bool allowMultipleValues,
            string siteName, string webName, string lookupListName, string lookupField, List<string> DependentLookupFieldNames)
        {
            using (SPSite spSite = new SPSite(siteName))
            {
                using (SPWeb spWeb = spSite.OpenWeb(webName))
                {
                    SPList lookupList = spWeb.Lists[lookupListName];

                    spWeb.Fields.AddLookup(fieldStaticName, lookupList.ID, lookupList.ParentWeb.ID, required);
                    SPFieldLookup lookup = spWeb.Fields[fieldStaticName] as SPFieldLookup;
                    lookup.Title = fieldDisplayName; // "Title" is Display Name
                    lookup.AllowMultipleValues = allowMultipleValues;
                    lookup.LookupField = lookupField;
                    lookup.Group = group;
                    lookup.Indexed = true;
                    //lookup.RelationshipDeleteBehavior = SPRelationshipDeleteBehavior.Restrict;//"A site column cannot enforce a relationship behaviour"
                    lookup.StaticName = fieldStaticName;
                    lookup.Update(true);

                    if (DependentLookupFieldNames != null)
                    {
                        // Create the secondary columns. ie what's editable via the web ui reading:
                        // "Add a column to show each of these additional fields:" Displays like "Committee:ID"
                        // ie depField will usually be "ID"
                        foreach (string depField in DependentLookupFieldNames)
                        {
                            string dependantFieldName = spWeb.Fields.AddDependentLookup(string.Format("{0}:{1}", fieldDisplayName, depField), lookup.Id);// eg "Committee:ID"
                            SPFieldLookup dependentField = spWeb.Fields.GetFieldByInternalName(dependantFieldName) as SPFieldLookup;
                            dependentField.LookupField = depField;
                            dependentField.Group = group;
                            dependentField.Update(true);
                        }

                        spWeb.Update();
                    }

                    return lookup;
                }
            }
        }