Monday, February 27, 2012

Working With Windows Phone 7

After Windows Phone 7.1 SDK was installed, a list of Silverlight-based WP project templates will be available in Visual Studio 2010. Then you are ready to create your WP application using Sliverlight(XAML/C#). Yes WP just looks like a special Sliverlight client. You can simply test your WP application simply by the built-in emulator.

I got a Samsung Focus WP handset. I created a simple app and hoped easily to deploy it to the handset device. I thought it's trivial since I found "Windows Phone Device" option right beside the debug button on VS2010 menu. But it's not as easy as I thought.

I connected the handset to the dev machine with a USB cable, then click run with "Windows Phone Device" option. Bingo I got an error telling me that Zune software is not installed. Why deploying a WP app has business with the notorious Zune? Just copying the bad ideal of tethering iPhone on iTune from Apple? Is iZune or iTone a better name for that? No idea what MS is thinking about. Anyway download a 100M Zune package, install it in the dev machine and reboot the machine.

Ready to deploy my own app to the external handset right? The answer is NO! I got another error "Failed to connect to device as it is developer locked". Fantastic the Samsung phone is "locked". Is it locked by MS, Samsung or the carrier? I don't know. But it doesn't look like a carrier lock as iPhone's case.

To unlock a smartphone device, you will have to register a App Hub account, basically a WP/XBOX developer membership costing $99 a year. By paying a hundred bucks, you are eligible to unlock three physical smartphone devices the most. Who figured out the number of 3, instead of 5, 10 or whatever? Guess people from WP team are quite sure that there won't be more than three WP devices around you as a developer. It’s just ridiculous.

So you have a smartphone device, and you want to do some homebrew stuff for yourself and don't want to pay $99/year fee, what should you do? No luck at all! You may end up to an Android device.

WP market share is less than 5 percent, almost ignorable. It's so low that developers are just not willing to work on it. Should MS just make WP development a bit easier in such case? Sigh...

Friday, February 17, 2012

Replacing SharePoint 2010 Content Editor WebPart Links

This console app is to replace those hard-coded URLs inside content editor web parts. In previous post we showed how to extend https://proudctionServer application to https://stagingServer, but some URLs inside the content editor web parts in https://stagingServer are still pointing to https://productionServer. This console will replace them as desired staging URLs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.SharePoint.Administration;

namespace ContentEditorWPLinkUpdate
{
/// <summary>
/// This console application is to replace hard-coded links inside ContentEditorWebPart.
/// e.g.
/// <a href="https://productionServer/news/abcd">abcd news</a>
/// will be replaced by:
/// <a href="/news/abcd">abcd news</a>
/// Run the executable directly or run it with optional parameters:
/// Cmd:\>ContentEditorWPLinkUpdate [originalLink] [newLink]
/// </summary>
class Program
{
readonly static string webApplication = "https://productionServer/";
static string oldUrl = "https://productionServer/";
static string newUrl = "/";
static string hrefLinkTeReplaced = string.Format("href=\"{0}", oldUrl);
static string srcLinkToBeReplaced = string.Format("src=\"{0}", oldUrl);

static void Main(string[] args)
{
if (args.Length > 0)
{
oldUrl = args[0].Trim().TrimEnd(new char[] { '/' }) + "/";
hrefLinkTeReplaced = string.Format("href=\"{0}", oldUrl);
srcLinkToBeReplaced = string.Format("src=\"{0}", oldUrl);
if (args.Length > 1)
newUrl = args[1].Trim().TrimEnd(new char[] { '/' }) + "/";
}

Console.WriteLine("Replacing URL {0} by {1} for content editor WebParts...", oldUrl, newUrl);

SPWebApplication webApp = SPWebApplication.Lookup(new Uri(webApplication));

foreach (SPSite siteCollection in webApp.Sites)
{
Console.WriteLine("Updating Site collection: " + siteCollection.Url);

foreach (SPWeb web in siteCollection.AllWebs)
{
UpdateWebRootFolderFiles(web);
UpdatePagesFiles(web);
web.Close();
}

siteCollection.Close();
}

Console.WriteLine("Content Editor WebPart links update completed!");
}

/// <summary>
/// Update URL in content editor WebPart
/// </summary>
static void UpdateContentEditorWebPart(SPLimitedWebPartManager wpManager, ContentEditorWebPart contentEditor)
{
string origContent = contentEditor.Content.InnerText;
if (origContent.Contains(hrefLinkTeReplaced) || origContent.Contains(srcLinkToBeReplaced))
{
XmlDocument xmlDoc = new XmlDocument();
XmlElement xmlElement = xmlDoc.CreateElement("HtmlContent");
xmlElement.InnerText = origContent.Replace(hrefLinkTeReplaced, "href=\"" + newUrl).Replace(srcLinkToBeReplaced, "src=\"" + newUrl);
contentEditor.Content = xmlElement;
wpManager.SaveChanges(contentEditor);
}
}

/// <summary>
/// Update files in sites' root folder
/// </summary>
static void UpdateWebRootFolderFiles(SPWeb web)
{
using (web)
{
// Loop through all files in root folder
if (web.RootFolder.Files != null)
{
foreach (SPFile webRootFile in web.RootFolder.Files)
{
try
{
if (webRootFile.Url.ToLower().EndsWith(".aspx"))
{
using (SPLimitedWebPartManager wpManager = webRootFile.GetLimitedWebPartManager(PersonalizationScope.Shared))
{
foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in wpManager.WebParts)
{
if (webPart != null && webPart.GetType().ToString().Contains("ContentEditorWebPart"))
{
ContentEditorWebPart contentEditor = (ContentEditorWebPart)webPart;
UpdateContentEditorWebPart(wpManager, contentEditor);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("Error occurs when updating {0} : {1}", webRootFile.ServerRelativeUrl, ex.Message));
}
}
}
}
}

/// <summary>
/// Update Publishing pages
/// </summary>
static void UpdatePagesFiles(SPWeb web)
{
SPList list = web.Lists.TryGetList("Pages");
if (list == null)
return;

SPFile page = null;
foreach (SPListItem listItem in list.Items)
{
if (!listItem.Url.ToLower().EndsWith(".aspx"))
continue;
try
{
page = web.GetFile(web.Url + "/" + listItem.Url);
bool needToUpdate = false;
using (SPLimitedWebPartManager wpManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
{
foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in wpManager.WebParts)
{
if (webPart.GetType().ToString().Contains("ContentEditorWebPart"))
{
ContentEditorWebPart contentEditor = (ContentEditorWebPart)webPart;

string content = contentEditor.Content.InnerText;
if (content.Contains(hrefLinkTeReplaced) || content.Contains(srcLinkToBeReplaced))
{
needToUpdate = true;
}
}
}
}
if (needToUpdate)
{
if (page.CheckOutType != SPFile.SPCheckOutType.None)
page.UndoCheckOut();
page.CheckOut();

string webPartTitles = string.Empty;
using (SPLimitedWebPartManager wpManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
{
foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in wpManager.WebParts)
{
if (webPart != null && webPart.GetType().ToString().Contains("ContentEditorWebPart"))
{
ContentEditorWebPart contentEditor = (ContentEditorWebPart)webPart;
UpdateContentEditorWebPart(wpManager, contentEditor);
}
}
}

page.CheckIn("Replace hard-coded URL for content editor web part:" + webPartTitles);
page.Publish("Replace hard-coded URL for content editor web part:" + webPartTitles);
}
}
catch (Exception ex)
{
Console.WriteLine(string.Format("Error occurs when updating {0} : {1}", page.ServerRelativeUrl, ex.Message));
}
}
}
}
}

Wednesday, February 15, 2012

SharePoint 2010 Solution Package Redeployment After Site Extended

Originally we had identical SharePoint 2010 setup in production and staging environments, and both were using the same URLs. In order to visit the staging environment, we need to change machine’s hosts file to point to staging servers. However some business people who need to test the staging server simply don’t have permission to change their hosts file by their own. So we decided to separate the production and staging URLs, one is production.company.com the other is staging.company.com.

When the new DNS entries for staging servers were ready, we started setting up the SharePoint Alternate Access Mapping (AAM) for staging servers. One requirement was that all access needs to be secure so we needed to extend the original web application. Following are steps of the configuration:

1. Open up central admin, go to application management, select the web application and click Extend button on the ribbon

2. In the popup window type following:
  • a. Port: 443
  • b. Host Header: staging.company.com
  • c. Zone: Intranet
  • d. Other as default

3. Go to Application Management -> Configure alternate access mappings -> Edit Public Zone URLs, select the web application and change http://staging.company.com:443 to https://staging.company.com

4. For each front-end server, open up the IIS manager and locate the newly extended site, write down the site ID something like 34561234, the open a command prompt as administrator:

C:\inetpub\AdminScripts>cscript adsutil.vbs set /w3svc/34561234/SecureBindings ":443:staging.company.com"

For more about SSL certificate setup refer to this and this.

5. Go back to IIS manager, select the extended site, and click Edit Bindings. Delete the http binding without SSL certificate and keep the other https one, and then start the IIS site (it’s stopped by default)

6. Optionally copy the original web.config to the extended IIS site if your solution packages are not taking care of all web.config changes.

7. Optionally set the hosts file so that the new DNS entries are pointing to itself, if you have a multiple front-end server farm and NTLM authentication is used. The service calls inside webpart or custom code could possibly route to other front-end server which could lead to the double-hop authentication issue (refer to this). Hosts file setup ensures those service calls always getting to local machine.

The story wasn't stopping yet. We noticed quite a few custom webparts and applications are not working properly after the Web Application is extended. Finally we found SharePoint would redeploy those farm solution packages that have content (such as dll) with deploy target of WebApplication. The redeployment sequence is random which would lead to some problem. In our case, the dll included in the one solution package overrides the newer dll in another solution package. So we redeployed all solution packages with proper order and then the issues were gone.