Colin's profileColin Brown MSN MVPPhotosBlogListsMore ![]() | Help |
|
|
August 29 Virtual PC does not support X64 guestsIt’s Microsoft rant time. I’m currently running a beefy (12Gb Ram, Intel i7 etc.) Windows 7 Ultimate X64 system and am trying to prepare for upcoming versions of Microsoft software that I know I will be testing and working with on a daily basis once they get released. Like any other developer I want to run these as VMs. I’m specifically talking about Sharepoint 2010 here which is 64 bit only. So I download both Virtual PC 2007 SP1 and also the latest Virtual PC for Windows 7 and come to find out that neither supports 64 bit guest operating systems. Come on Microsoft, what are you playing at? VMware has supported 64 bit guest operating systems for ages now. How do you expect developers to develop against your new systems which are going to be 64 bit only if they can’t set up their own development environments? I could perhaps understand from VPC 2007 SP1 but even their new Virtual PC for Windows 7 still only supports 32 bit guests. Microsoft wants to make a dent in VMware but things like this show just how forward thinking VMware is and how much Microsoft is not. Virtual PC for Windows 7 is still in the release candidate phase. Lets hope that before it actually goes live, Microsoft have the foresight to allow users to install 64 bit operating systems. Another sticking point (although not nearly as bad) is the limit under both Virtual PC environments to allow only 3.7Gb of RAM to be allocated to a Virtual Machine. This is also true by the way for Virtual Server. This limitation also needs to be taken out of these products. Come on Microsoft, get your act together and let the world know you can be taken seriously in the virtual arena. May 13 New ComputerWell I received my new computer last week. Everybody seems to be doing some kind of unboxing thing on the web nowadays so here is mine :- Here it is. Two absolutely massive boxes just waiting to be opened. Lets start with the smaller box It turned out to be the monitor. It’s a massive 24 inch display and as you can see can display resolutions up to 1920x1200 widescreen format. Built into the monitor are a couple of DVI ports, VGA, RCI and even an HDMI port that can display full hi-def 1080p television. As well as this there are a few usb ports and also two multi-function card readers. Next up the other box which should be the computer, even though it is the bigger of the two boxes :- The black box that came packaged contained the manuals, install DVDs, mouse, keyboard etc. Get it set up and ready to turn on for the first time. Yep, appears to work. Now whenever you get a new computer, what’s the first thing you do (apart from test to see if it works)? Install software? Configure it? Nope, take it apart :- I wanted to take it apart to add another Hard Drive to it. A 1Tb Western Digital Black drive. There we go. I also got a new 1Tb Cavalry External hard drive as well with USB and eSATA connections. This is a monster of a machine. Here are just a few of the specs :-
I upgraded from the Vista 64 Home Premium that came with the machine to Vista 64 Ultimate edition and ran the system rating tool topping it out at 5.9 for every reading. As you can see, not a bad system :) September 12 Telerik RadGrid ControlA few months ago I done a talk and demonstration at my local user group on what was then referred to as the Prometheus controls and are now the Telerik Rad Controls for ASP.Net Ajax. These are an excellent set of controls that I would definitely recommend any web developer go take a look at. As a very quick demonstration on their power, here's a very short example of what the RadGrid editor can do (and it can do a whole lot more than this). Their latest RadGrid control is Asp.Net Ajax enabled and can interact with your client side Javascript. The scenario is that we want to have a custom form for editing table rows in the grid rather than doing in-line editing and we also want a client side popup to happen when you mouse over a grid row to show additional details not displayed in the grid. The first part is very easy to do as it comes built in to the RadGrid control, you just have to enable it. For this demo I've just enabled the default options although you can completely customize the input form to your own tastes if you don't want the default settings. <telerik:RadGrid ID="RadGrid" runat="server" AllowPaging="True" AllowSorting="True" GridLines="None" Skin="Office2007" AutoGenerateDeleteColumn="True" AutoGenerateEditColumn="True" PageSize="1"> <MasterTableView EditMode="EditForms" ClientDataKeyNames="StockID"> <RowIndicatorColumn> <HeaderStyle Width="20px"></HeaderStyle> </RowIndicatorColumn> <ExpandCollapseColumn> <HeaderStyle Width="20px"></HeaderStyle> </ExpandCollapseColumn> </MasterTableView> <ClientSettings AllowColumnsReorder="True" ReorderColumnsOnClient="True"> <ClientEvents OnRowMouseOver="RowSelected" OnRowMouseOut="RowDeSelected" /> </ClientSettings> <FilterMenu EnableTheming="True"> <CollapseAnimation Type="OutQuint" Duration="200"></CollapseAnimation> </FilterMenu> </telerik:RadGrid> That's all there is to it. We declare out RadGrid control and add the EditMode="EditForms" attribute to the MasterTableView tag. Very simple. The next part is a bit more involved, but not complicated. As part of the RadGrid, you can expose certain columns so that they are picked up by your Javascript code. As you can see in the code above, I've enabled the OnRowMouseOver and OnRowMouseOut events in the ClientEvents tag. These will fire every time your mouse moves over or out of a row in the RadGrid and call a Javascript Routine. In the MasterTableView tag we have told the RadGrid to expose the StockID column to Javascript. function RowSelected(sender, args) {var id = args.getDataKeyValue("StockID"); var popup = document.getElementById("Popup"); TelerikService.GetAdditionalInfo(id, onSuccess, onerror); popup.style.display = "block"; popup.style.position = "absolute";popup.style.top = args._domEvent.clientY + 20; popup.style.left = args._domEvent.clientX; popup.style.width = "300px"; popup.style.height = "200px"; popup.style.backgroundColor = "FFFDA3";} function RowDeSelected(sender, args) {var popup = document.getElementById("Popup"); popup.style.display = "none";} These are the two functions that it calls. The first calls a web service to receive further details then simply updates a hidden Div with the information and displays it. The second function simply re-hides the Div again. To get the value that the RadGrid passes in, you simply query it against the args collection that's captured as part of the function call arguments. var id = args.getDataKeyValue("StockID"); And that's really all there is to it :- For the Ajax web service call you need to add a couple of other Javascript routines and also place the web service reference in the Services tag of the Ajax ScriptManager tag. Here is the full HTML :- <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <%@ Register Assembly="Telerik.Web.UI, Version=2008.2.723.35, Culture=neutral, PublicKeyToken=121fae78165ba3d4" Namespace="Telerik.Web.UI" TagPrefix="telerik" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Test Telerik Control</title> <script language="javascript" type="text/javascript"> function RowSelected(sender, args) {var id = args.getDataKeyValue("StockID"); var popup = document.getElementById("Popup"); TelerikService.GetAdditionalInfo(id, onSuccess, onerror); popup.style.display = "block"; popup.style.position = "absolute";popup.style.top = args._domEvent.clientY + 20; popup.style.left = args._domEvent.clientX; popup.style.width = "300px"; popup.style.height = "200px"; popup.style.backgroundColor = "FFFDA3";} function RowDeSelected(sender, args) {var popup = document.getElementById("Popup"); popup.style.display = "none";} function onError(result) { alert("WebService failed : " + result.get_Message());} function onSuccess(result, userContext) {var res = new Array(); res = result.split("|");var symbol = $get("StockSymbol"); symbol.innerHTML = res[0]; var price = $get("Price"); price.innerHTML = res[1]; var category = $get("Category"); category.innerHTML = res[2]; var shares = $get("SharesAvailable"); shares.innerHTML = res[3]; } </script> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server"> <Services> <asp:ServiceReference Path="~/TelerikService.asmx" /> </Services> </asp:ScriptManager> <div> <telerik:RadAjaxLoadingPanel ID="RadAjaxLoadingPanel" runat="server" Height="75px" Width="75px"> <img alt="Loading..." src='<%= RadAjaxLoadingPanel.GetWebResourceUrl(Page, "Telerik.Web.UI.Skins.Default.Ajax.loading.gif") %>' style="border: 0px;" /> </telerik:RadAjaxLoadingPanel> <telerik:RadGrid ID="RadGrid" runat="server" AllowPaging="True" AllowSorting="True" GridLines="None" Skin="Office2007" AutoGenerateDeleteColumn="True" AutoGenerateEditColumn="True" PageSize="1"> <MasterTableView EditMode="EditForms" ClientDataKeyNames="StockID"> <RowIndicatorColumn> <HeaderStyle Width="20px"></HeaderStyle> </RowIndicatorColumn> <ExpandCollapseColumn> <HeaderStyle Width="20px"></HeaderStyle> </ExpandCollapseColumn> </MasterTableView> <ClientSettings AllowColumnsReorder="True" ReorderColumnsOnClient="True"> <ClientEvents OnRowMouseOver="RowSelected" OnRowMouseOut="RowDeSelected" /> </ClientSettings> <FilterMenu EnableTheming="True"> <CollapseAnimation Type="OutQuint" Duration="200"></CollapseAnimation> </FilterMenu> </telerik:RadGrid> </div> <div id="Popup" style="display: none; position: absolute;"> <ul> <li id="StockSymbol"></li> <li id="Category"></li> <li id="Price"></li> <li id="SharesAvailable"></li> </ul> </div> <telerik:RadAjaxManager runat="server"> <AjaxSettings> <telerik:AjaxSetting AjaxControlID="RadGrid"> <UpdatedControls> <telerik:AjaxUpdatedControl ControlID="RadGrid" LoadingPanelID="RadAjaxLoadingPanel" /> </UpdatedControls> </telerik:AjaxSetting> </AjaxSettings> </telerik:RadAjaxManager> </form> </body> </html> As you can see, all fairly straightforward. I've also added Ajax support to the RadGrid through Telerik's own Ajax manager and display a nice animated image whenever the grid is being updated through Ajax, e.g. changing pages. The code beside page mostly makes a call to a Web service to get the data and bind it to the RadGrid control :- using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.UI;using System.Web.UI.WebControls;using Telerik.Web.UI;using TelerikSite;using TelerikWS;public partial class _Default : System.Web.UI.Page {protected void Page_Load(object sender, EventArgs e) { //Get some data TelerikWS.Stock[] stock;List<Stock> stocks = new List<Stock>(); TelerikWS.StockClient stockws = new TelerikWS.StockClient(); try {stock = stockws.GetAllStockDetails(); foreach (Stock s in stock) {stocks.Add(s); } RadGrid.DataSource = stocks; RadGrid.PagerStyle.Position = GridPagerPosition.Bottom; RadGrid.PagerStyle.Mode = GridPagerMode.NextPrevNumericAndAdvanced;RadGrid.DataBind(); } catch (TelerikSite.SQLDBAccessException ex) { string exc = ex.Message.ToString();} catch (Exception ex) { string exc = ex.Message.ToString();} } } SQLDBAccessException is a simple custom exception that I coded and StockService is a standard WCF web service which implements a Stock class. The Ajax Web Service is a simple asmx file that calls the main web service in turn (the reason for this is that the main web service might live in a different domain which Ajax doesn't handle too well at present due to security restrictions). using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Services;using System.Web.Services.Protocols;using System.Text;using TelerikWS;/// <summary> /// Summary description for TelerikService /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. [System.Web.Script.Services.ScriptService]public class TelerikService : System.Web.Services.WebService { public TelerikService() { //Uncomment the following line if using designed components //InitializeComponent(); } [WebMethod]public string GetAdditionalInfo(int ID) {string str = string.Empty; TelerikWS.Stock stock = new Stock(); try {TelerikWS.StockClient client = new StockClient(); stock = client.GetStockByID(ID); str = SerielizeStock(stock); } catch (Exception ex) { return ex.Message.ToString();} return str;} private string SerielizeStock(TelerikWS.Stock stock) {StringBuilder str = new StringBuilder(); str.Append("StockSymbol:");str.Append(stock.StockSymbol); str.Append("|StockPrice:");str.Append(stock.StockPrice.ToString()); str.Append("|StockCategory:");str.Append(stock.MainCategory); str.Append("|SharesAvailable:");str.Append(stock.SharesAvailable.ToString()); return str.ToString();} } July 11 Cheap and Easy Online HelpdeskA lot of professional sites are now including a means of real time chat as part of their helpdesk sites. MSDN has their online concierge service, Dell has an online help, AT&T etc. So how do you do this type of thing within your own web site? In previous articles I've covered how to incorporate Windows Live Messenger into your own site. This article simply takes that process one step further by allowing you to host your own online Helpdesk chat system. The code in this article is actually very similar to the code in the previous articles I've posted and so I won't be going over all the functions again, instead I will point out the differences between this application and the previous one.
As you can see, the site structure is exactly the same. The Privacy.htm and Channel.htm required files are present. There are a few graphics files, a theme and everything else is contained in the Default.aspx file. The main body of the html code found in Default.aspx is the same as in my previous articles however we've removed a couple of features as these aren't strictly necessary for a helpdesk application :-
<body onload="scriptMain()"> <form id="form1" runat="server"> <div id="msgr"> <table> <tr><td> <div id="signinframe"></div> </td></tr> <tr><td> <div id="userInfo"></div> </td></tr> <tr><td> <div id="sendMessage"> <hr /> <span><b>Send a Message:</b></span> <p id="contactLabel"></p> <p id="msgLastRecv"></p> <div id="txtConv"></div><br /> <input id="txtMessage" type="text"/><br /> <input onclick="sendMsg()" id="btnSend" type="button" value="Send Message" disabled="disabled" /> </div> </td></tr> <tr><td> <div id="divConversations"></div> </td></tr> <tr><td> <div id="Contacts"> <p><b>Helpdesk Contact <a href="Javascript:ToggleContacts();"> <img alt="ToggleContacts" src="minus_icon.gif" id="ContactsExpand" class="ContactImage" /> </a> </b></p> <div id="divContacts"></div> </div> </td></tr> </table> </div> </form> </body> As you will notice there no longer is a placeholder to change your online status and I've also removed the placeholder that allowed you to change your personal message. The rest however is the same. Once rendered it should look like this :- The basis of this program is that you go to a specific page on a site if you are having a problem and chat to someone from the site to help solve your problem. You want this to all be automatic and for your user to do as little as possible to enable this. With the upcoming launch of Windows Live Messenger 9, this actually becomes scalable to a point as Messenger 9 allows for multiple presences on the same Live ID. Therefore you could have multiple team members, all sign into the same Live ID and all handling chats at the same time. The user comes along to your site where you have your Live Web Messenger application and is prompted to sign-in. This is the same process and code as in the previous article so I won't regurgitate it here. Basically they are taken away from your site to the Windows Live login site, they enter their credentials and are then taken back to your site. The sign-in screen actually gets presented to them as a pop-up window so as not to completely take them away from your site. You application changes to reflect the fact that the user is signing in. At this point the application is awaiting the token back from the Windows Live Login site saying that the user is authenticated. Once signed in the user is able to chat to the help desk representative :- As you will notice from the above screen shot, we don't display all of the users contacts, we only display the help desk contact. Why do we even want to display this? Well it's helpful to see whether someone is actually signed on and manning the online help desk or not, as is reflected in the status displayed next to the contact (in the above example the person is not currently online). So what changes in the code do we need from my previous articles to get this to work? Basically it all happens in the displayContacts() routine :-
function displayContacts() {var sb = new StringBuilder(); var HelpDeskIndex = null; _addressList = new Array(_user.get_contacts().get_count());var groupList = new Array(_user.get_groups().get_count()); var enum2 = _user.get_groups().getEnumerator(); var enum1 = _user.get_contacts().getEnumerator(); var groupindex = 0;index = 0; while (enum2.moveNext()) { var c = enum2.get_current();var _contacts = new Array(c.get_contacts().get_count()); var enum3 = c.get_contacts().getEnumerator(); while (enum3.moveNext()) { var contact = enum3.get_current(); var address = contact.get_currentAddress(); var dispName = address.get_presence().get_displayName(); var currAddress = address.get_address(); var status = Enum.toString(Microsoft.Live.Messenger.PresenceStatus, address.get_presence().get_status()); //Check to see if Helpdesk account is presentif(currAddress.toLowerCase() == "Your helpdesk address") { HelpDeskAdded = true;var statusimg = null; switch(status) {case "online": statusimg = "<img src='online.gif' class='ContactImage' alt='online' />"; break;case "offline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "appearOffline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "away": statusimg = "<img src='away.gif' class='ContactImage' alt='away' />"; break;case "beRightBack": statusimg = "<img src='away.gif' class='ContactImage' alt='beRightBack' />"; break;case "busy": statusimg = "<img src='busy.gif' class='ContactImage' alt='busy' />"; break;case "idle": statusimg = "<img src='away.gif' class='ContactImage' alt='idle' />"; break;case "inACall": statusimg = "<img src='busy.gif' class='ContactImage' alt='InACall' />"; break;case "outToLunch": statusimg = "<img src='away.gif' class='ContactImage' alt='OutToLunch' />"; break; default: statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;} var statusLine = ''; var strDelete = ''; _addressList[index] = address; if (dispName !== '') {statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + dispName + '</a>'; } else {statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + currAddress + '</a>'; } sb.append(statusLine); HelpDeskIndex = index; } index++; } groupindex++; index = 0; while (enum1.moveNext()) { var contact = enum1.get_current(); var address = contact.get_currentAddress(); var dispName = address.get_presence().get_displayName(); var currAddress = address.get_address(); var status = Enum.toString(Microsoft.Live.Messenger.PresenceStatus, address.get_presence().get_status());if(currAddress.toLowerCase() == "Your helpdesk address") { HelpDeskAdded = true; //Change status to an imagevar statusimg = null; switch(status) {case "online": statusimg = "<img src='online.gif' class='ContactImage' alt='online' />"; break;case "offline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "appearOffline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "away": statusimg = "<img src='away.gif' class='ContactImage' alt='away' />"; break;case "beRightBack": statusimg = "<img src='away.gif' class='ContactImage' alt='beRightBack' />"; break;case "busy": statusimg = "<img src='busy.gif' class='ContactImage' alt='busy' />"; break;case "idle": statusimg = "<img src='away.gif' class='ContactImage' alt='idle' />"; break;case "inACall": statusimg = "<img src='busy.gif' class='ContactImage' alt='InACall' />"; break;case "outToLunch": statusimg = "<img src='away.gif' class='ContactImage' alt='OutToLunch' />"; break; default: statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;} var statusLine = ''; var strDelete = ''; _addressList[index] = address; if (dispName !== '') {statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + dispName + '</a>'; } else {statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + currAddress + '</a>'; } sb.append(statusLine); HelpDeskIndex = index; } index++; } groupindex++; } document.getElementById('divContacts').innerHTML = sb.toString();if(HelpDeskAdded == true) {createConv(HelpDeskIndex); } else {alert(HelpDeskAdded); var helpDeskContact = "Your helpdesk address"; if(_user !== null) {alert(_user); _user.addContact(helpDeskContact, "Please accept in order to converse with the helpdesk", null); HelpDeskAdded = true;} } } Overall this routine is very similar to the displayContacts() routine explained thoroughly in my previous articles however there are a few changes and also a hack (workaround) to a problem I came across, which I'll get to in a minute. if(currAddress.toLowerCase() == "your helpdesk address") Simply replace "your helpdesk address" with your help desks live id. Make sure however that it is all in lower case as you'll see from the line of code that that's how the code does the comparison. If the contact is found then we get the contacts status and create a string to display their information in the contacts section at the bottom of the application. Now you'll notice from the code that this basic routine looks like it's present twice. This is actually the quick hack. The reason for this is the way Live Contacts can be stored. Most contacts are actually assigned to a group and you may have multiple groups with multiple contacts in each. However some contacts are actually stored outside of groups and are in effect free floating. This hack gets around that problem. The first routine searches through all the contacts that belong to groups, the second section (which is essentially the same code) searches through the contacts that aren't assigned to any group. if(HelpDeskAdded == true) {createConv(HelpDeskIndex); } else {var helpDeskContact = "Hackersoft@live.com"; if(_user !== null) {_user.addContact(helpDeskContact, "Please accept in order to converse with the helpdesk", null); HelpDeskAdded = true;} } At the bottom we have the check to see whether the help desk contact has been added or not. If it's already present then we are ready to start a conversation, if not then we need to add the help desk contact to the users contact list. The HelpDeskAdded variable is a global variable defined later in the code. And that's basically it. One last point, I came across a bug in my previous articles. Before I had the :- index = 0; placed within the loop, this actually caused problems when trying to chat with the correct contact. This has now been moved outside of the loop. For the complete source file see this post (due to limitations in Live Spaces) With this you now have a fully functional interactive help desk chat application that you can add to your site. May 21 How to use Teleriks' RadChart control within SharepointTechnorati Tags: Telerik, RadChart, Sharepoint 2007, MOSS 2007, Web Charts in Sharepoint, Telerik RadChart in Sharepoint I have been playing around with the Telerik controls over the past couple of months and decided to see if I could incorporate their RadChart control into Sharepoint. The basis was to create a custom web part that houses the RadChart control. That web part then points to a list and gets its data from the list and creates the chart. It's a fairly easy process but there are a couple of gotchas to watch out for. So let's get started. The first thing you need to do is to download the "Dev" zip file from Telerik so that you can do a manual deployment, rather than the normal automated MSI file. Once you have that, unzip it. There are two things you need to deploy from this. Open up Windows Explorer on your Sharepoint box and browse to your Sharepoint Site directory (usually found under c:\inetpub\wwwroot\wss\VirtualDirectories somewhere). Then open up the bin directory as this is where you will copy the Web Chart dll. Next you need to register the RadChart so that it's marked as safe in the web.config file. Open the web.config file for your Sharepoint site and add the following to the end of the SafeControls list :- <SafeControl Assembly="RadChart.Net2, Version=4.1.1.0, Culture=neutral, PublicKeyToken=d14f3dcc8e3e8763" Namespace="Telerik.WebControls" TypeName="*" Safe="True" /> This is the RadChart 2007 Q3 SP1 version. If you've got a different version of the RadChart then these values will be slightly different, namely the version number and the PublicKeyToken. Now Sharepoint can theoretically display charts, but without some custom coding it obviously won't be able to. Next open your copy of Visual Studio and create a class file (I actually used Visual Studio 2005 SP1 for this and download the Sharepoint Extensions for Visual Studio 2005 and used the Sharepoint Web Part project to build this with. Makes things slightly easier). Once you have your project open in Visual Studio, you need to include a reference to the RadChart dll (In solution explorer, right click and select Add Reference then browse to where the dll resides). Now to the code :- using System; using System.Runtime.InteropServices; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Serialization; using Telerik.WebControls; using Telerik.Charting; using System.Runtime.CompilerServices; using System.Reflection; using System.Security; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using System.Collections.Generic; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; using Microsoft.SharePoint.WebPartPages; [assembly:AllowPartiallyTrustedCallers] This first part are the includes that you will need along with the AllowPartiallyTrustedCallers attribute which is required for Sharepoint. namespace WebChart { [Guid("2bee1192-339c-4f85-849d-2df6f24c24d4")] public class WebChartPart : System.Web.UI.WebControls.WebParts.WebPart { #region Private Properties private string m_Web = string.Empty; private string m_Site = string.Empty; private string m_List = string.Empty; private string m_ChartData = string.Empty; private string m_ChartText = "Title"; #endregion Next we define our class and namespace and also create some local variables. These variables will be used by public properties that will show up Sharepoint to allow users to set which list, columns etc. to get the data from. #region Editor Properties [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Web), */ Personalizable(PersonalizationScope.Shared), DisplayName("MOSS Site"), WebDisplayName("MOSS Site"), WebDescription("The Top Level MOSS Site where the list resides.")] public string Web { get { return m_Web; } set { m_Web = value; } } [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Site), */ Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("Sub Site"), WebDescription("The actual MOSS Site where the list resides.")] public string MOSSSite { get { return m_Site; } set { m_Site = value; } } [WebBrowsable(true), Category("Chart Settings"), Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("List Name"), WebDescription("The list where the chart data resides.")] public string MOSSList { get { return m_List; } set { m_List = value; } } [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Site), */ Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("Chart Data"), WebDescription("The numeric column within the list to get the data.")] public string ChartData { get { return m_ChartData; } set { m_ChartData = value; } } [WebBrowsable(true)] [Category("Chart Settings")] /* [DefaultValue(m_Site)] */ [Personalizable(PersonalizationScope.Shared)] [DisplayName("Sub Site")] [WebDisplayName("Chart Text")] [WebDescription("The text column within the list to get the chart text.")] public string ChartText { get { return m_ChartText; } set { m_ChartText = value; } } #endregion And here are the public properties. One for each of the private variables we declared earlier. These all follow the same procedure. First you decorate the property with attributes that tell Sharepoint which grouping to list them under in the Edit Properties pane, what you wish to call this property, how to store the values within Sharepoint etc. Then we just have a standard getter/setter pair pointing back to our private variables. Next we need to instantiate our RadChart control :- #region Controls Telerik.WebControls.RadChart chart = new RadChart(); #endregion We don't need to do anything with the class constructor. However we do need to override the CreateChildControls method. This is where we will be adding the RadChart to the web parts controls for display :- public WebChartPart() { } protected override void CreateChildControls() { base.CreateChildControls(); if (m_List != string.Empty) { GetSites(); //GetFolders(); chart.UseSession = false; chart.RadControlsDir = "~/_wpresources/RadControls"; chart.TempImagesFolder = "~/_wpresources/RadControls/Chart/TempImages"; BuildChart(); this.Controls.Add(chart); } } First we make sure any other controls and created, then check to see if the List public property actually has a value. If not then there obviously won't be any data for the chart so we simply don't display the chart. If it does have a value then we get all the data to build the chart and then display the control. There are a couple of things worthy of mention here :-
As noted above there is something else that you need to copy over from your RadControls zip file. Go to where you unzipped all the controls then go to the Net2\RadControls directory. Within there you will find a Chart folder. Copy this folder. Next to go c:\program files\common files\microsoft shared\web server extensions\wpresources and create a folder in there called RadControls. Paste the Chart directory you just copied into the RadControls folder. Back to Visual Studio and our next method :- private void GetSites() { if (m_Web == string.Empty) { SPWeb myweb = SPControl.GetContextWeb(this.Context); m_Web = myweb.Url; } } All that this method does is pre-populate the web site public property with the url of the current site where the web part is deployed. private void BuildChart() { ArrayList numbers = new ArrayList(); ArrayList text = new ArrayList(); SPWeb myweb = SPControl.GetContextWeb(this.Context); SPList list = myweb.Lists[m_List]; for (int i = 0; i < list.ItemCount; i++) { SPListItem item = list.Items[i]; numbers.Add(Convert.ToInt32(item[m_ChartData].ToString())); text.Add(item[m_ChartText].ToString()); } chart.ChartTitle.TextBlock.Text = list.Title; ChartSeries series = new ChartSeries(); series.Name = list.Title; series.Type = ChartSeriesType.Pie; for (int i = 0; i < numbers.Count; i++) { int j = Convert.ToInt32(numbers[i]); series.AddItem(j, Convert.ToString(text[i])); } chart.Series.Add(series); } } } Finally we have our BuildChart method. We first create two lists, one to hold the text to be displayed and one to hold the numerical data that will make up the chart. Next we fill these two arrays from the list pointed to by the values entered in the Edit Web Part Properties window that the user will enter. Compile your dll, put it in your Sharepoint sites bin folder and add it to the safecontrols. if you use the code below then your safecontrol entry should look similar to :- <SafeControl Assembly="WebChart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5" Namespace="WebChart" TypeName="*" Safe="True" /> Add your web part to the page, edit the web part properties to point to a valid site, list and data and there you have it. The sample list I used I only put two values in for testing and here are the results :- You now have a fully functional RadChart control that can be embedded in your Sharepoint sites. Here is the full code for the above :- using System; using System.Runtime.InteropServices; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Serialization; using Telerik.WebControls; using Telerik.Charting; using System.Runtime.CompilerServices; using System.Reflection; using System.Security; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using System.Collections.Generic; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; using Microsoft.SharePoint.WebPartPages; [assembly:AllowPartiallyTrustedCallers] namespace WebChart { [Guid("2bee1192-339c-4f85-849d-2df6f24c24d4")] public class WebChartPart : System.Web.UI.WebControls.WebParts.WebPart { #region Private Properties private string m_Web = string.Empty; private string m_Site = string.Empty; private string m_List = string.Empty; private string m_ChartData = string.Empty; private string m_ChartText = "Title"; #endregion #region Editor Properties [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Web), */ Personalizable(PersonalizationScope.Shared), DisplayName("MOSS Site"), WebDisplayName("MOSS Site"), WebDescription("The Top Level MOSS Site where the list resides.")] public string Web { get { return m_Web; } set { m_Web = value; } } [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Site), */ Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("Sub Site"), WebDescription("The actual MOSS Site where the list resides.")] public string MOSSSite { get { return m_Site; } set { m_Site = value; } } [WebBrowsable(true), Category("Chart Settings"), Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("List Name"), WebDescription("The list where the chart data resides.")] public string MOSSList { get { return m_List; } set { m_List = value; } } [WebBrowsable(true), Category("Chart Settings"), /* DefaultValue(m_Site), */ Personalizable(PersonalizationScope.Shared), DisplayName("Sub Site"), WebDisplayName("Chart Data"), WebDescription("The numeric column within the list to get the data.")] public string ChartData { get { return m_ChartData; } set { m_ChartData = value; } } [WebBrowsable(true)] [Category("Chart Settings")] /* [DefaultValue(m_Site)] */ [Personalizable(PersonalizationScope.Shared)] [DisplayName("Sub Site")] [WebDisplayName("Chart Text")] [WebDescription("The text column within the list to get the chart text.")] public string ChartText { get { return m_ChartText; } set { m_ChartText = value; } } #endregion #region Controls Telerik.WebControls.RadChart chart = new RadChart(); #endregion public WebChartPart() { } protected override void CreateChildControls() { base.CreateChildControls(); if (m_List != string.Empty) { GetSites(); //GetFolders(); chart.UseSession = false; chart.RadControlsDir = "~/_wpresources/RadControls"; chart.TempImagesFolder = "~/_wpresources/RadControls/Chart/TempImages"; BuildChart(); this.Controls.Add(chart); } } private void GetSites() { if (m_Web == string.Empty) { SPWeb myweb = SPControl.GetContextWeb(this.Context); m_Web = myweb.Url; } } private void BuildChart() { ArrayList numbers = new ArrayList(); ArrayList text = new ArrayList(); SPWeb myweb = SPControl.GetContextWeb(this.Context); SPList list = myweb.Lists[m_List]; for (int i = 0; i < list.ItemCount; i++) { SPListItem item = list.Items[i]; numbers.Add(Convert.ToInt32(item[m_ChartData].ToString())); text.Add(item[m_ChartText].ToString()); } chart.ChartTitle.TextBlock.Text = list.Title; ChartSeries series = new ChartSeries(); series.Name = list.Title; series.Type = ChartSeriesType.Pie; for (int i = 0; i < numbers.Count; i++) { int j = Convert.ToInt32(numbers[i]); series.AddItem(j, Convert.ToString(text[i])); } chart.Series.Add(series); } } } April 03 Cyberlink PowerDVD 8 is here
I must admit that I've been a big fan of Cyberlink's PowerDVD player for years now. Most people get their introduction to it through a stripped down version that comes free with some computers. I've been using PowerDVD as my main DVD player since version 3 and watched it grow over the years supporting more and more formats. Yesterday Cyberlink announced the release of their latest version of PowerDVD, version 8. Just take a look at the screen shot above to see all that is now supported with the latest release. Some people will ask, why do I need a separate movie player when Windows already comes with WMP? The answer to that lies in how you wish to watch your movies and what technologies it uses. WMP for example doesn't support Dolby digital, cannot play Blue Ray movies etc. where as with PowerDVD, it's like you're sitting in your own theatre (with the appropriate equipment of course). However Cyberlink's PowerDVD isn't just for playing your DVDs as it supports a number of different formats, AVI, DivX and Xvid to name a few. Both picture quality and sound quality are superb as you would expect. PowerDVD uses hardware acceleration when it can to give you smooth flowing videos without interruption. One thing however is when trying to connect to an external device I had plugged in (Archos 605 media player) through USB, PowerDVD crashed when I tried to list my UPnP devices :- This wasn't very encouraging for a newly released piece of software and something I'll need to dig into more. PowerDVD offers you a great number of features and is very customizable including skinning if you don't like the default move player look (see above). I'll be taking a look at all these features and more in an upcoming review. In the meantime, please check this out. A trial version is available for download from Cyberlink March 14 Windows Live Messenger Web Controls - Part 2Technorati Tags: Windows Live, Windows Live Messenger, Windows Live Messenger API, Programming, Web based Windows Live Messenger Hopefully you will have read and followed the code from the first part of this article. The first part covered most of base functionality including how to sign in, get the users details and get their contact list. What is left is how to actually carry out a conversation with a contact. Unfortunately I'm receiving errors when I try to post this article up here on Windows Live Spaces therefore to see this second part of the tutorial on how to integrate Windows Live Messenger into your own web site, please go :- http://www.liveside.net/blogs/developer/archive/2008/03/14/windows-live-messenger-web-controls-part-2.aspxMarch 13 Windows Live Messenger Web Controls - Part 1Technorati Tags: Windows Live Messenger, Windows Live Messenger API, Adding Messenger to your website, Windows Live Messenger Web Controls The Windows Live team have been busy over the past few months and recently launched the Windows Live Messenger APIs. This is basically split over three parts, the IM Control, the Windows Live Presence API and the Windows Live Messenger Library. What all this allows you to do is to create a Windows Live Messenger plug-in for your own website. What this (and coming articles) will try to explain is how to use these API's to actually create and integrate Messenger on your site as you can see below. Getting Started with the Windows Live Sign-in ControlI've covered this topic in a previous article I've written but I'll do a quick overview once again. In order to use Messenger you first need to sign-in to Windows Live (http://login.live.com). In order to sign-in you must host the sign-in control on your site and in order to host the sign-in control you need the following two files :- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html > <head> <title>Channel</title> <meta name="ROBOTS" content="NONE"/> <script type="text/javascript"> try { var hash = window.location.hash.substr(1);if (window.location.replace == null) window.location.replace = window.location.assign; window.location.replace("about:blank");var name = hash.split("/")[0]; var win = null; if (name && (name != ".parent")) win = window.parent.frames[name]; elsewin = window.parent.parent; if (win.Microsoft) {win.Microsoft.Live.Channels.Mux._recv_chunk(hash); } } catch (ex) { /* ignore */} </script> </head> <body></body> </html> The Privacy.htm file is just what it sounds like. It is the privacy policy for your actual website. In this page you are fairly much free to put whatever you wish, as an example :- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html > <head> <title>Privacy Policy</title> <meta name="ROBOTS" content="NONE"/> </head> <body> <h1>No information is actually stored on this website.</h1> </body> </html> For more information on the privacy.htm file and guidance on what information to put it in, please see http://privacy.microsoft.com/en-us/default.aspx. Next you need to register your website with Windows Live (this means that you must have a valid domain name/website). You do this by :- Logging into https://msm.live.com/app/default.aspx Select "Register and application" from the menu on the side and complete the details that it asks. Once you've completed all the information, it will give you an Application ID. You can also check all the Application IDs that you currently have from this site by clicking on the "Manage my applications" link on the side. Keep that application ID and also Secret Key that you typed in on stage 2 as you will need these later on. Asp.Net AjaxHow you wish to implement Messenger into your site is really up to you. You can place it into a web control (.ascx file), code it into a server control (.dll) or just put the code into one of your pages. For this article I'll simply be using a blank page so as not to confuse you with any other code. At the end of the day we wish to end up with something like the following :- My site structure is extremely simple, I have the Privacy.htm and Channel.htm files at the top level of my site. The site only consists of a single page (default.aspx), a few graphics files, a theme and of course the web.config file as follows :- As all of this is client side, you don't need to put any additional code in your code behind file, so all the work we will be doing will be in the default.aspx file itself. One thing you will need before going any further however is a good knowledge of not just Javascript but also of Microsoft's Asp.Net Ajax as the messenger API relies heavily on this. In fact, if you did not know Asp.Net Ajax, the Messenger API's would really be of no use to you. Why do I say this? Take the following as an example. Here is a page from the Messenger API :- This snippet (which you can find here) is the Properties associated with the Group class. As you can see Group has two properties, Contacts and Name. If you tried to use these as is on your web page then you would end up with a Javascript error. Why? Because these properties are actually Asp.Net Ajax properties, and in order to access Asp.Net Ajax properties you have to prefix them with "get_" and attach brackets to the end. For example, if you wanted to use the name property your code would actually have to be :- var groupName = get_name(); IntegrationNow that we have all this out the way, lets get started with our Default.aspx page. Before delving into the javascript, lets layout the actual page first. <body onload="scriptMain()"> <form id="form1" runat="server"> <div id="msgr"> <table> <tr><td> <div id="signinframe"></div> </td></tr> <tr><td> <div id="userInfo"></div> </td></tr> <tr><td> <div id="setUserStatus"> <span><b>Change Your Status:</b></span> <select id="selectStatus" onchange="selectStatusChanged()"> <option>Appear Offline</option> <option>Away</option> <option>Be Right Back</option> <option>Busy</option> <option>Idle</option> <option>In a Call</option> <option>Online</option> <option>Out to Lunch</option> </select> </div> </td></tr> <tr><td> <div id="setPersonalMessage"> <span><b>Personal Message: </b></span> <input id="personalMessage" type="text" /> <input onclick="setPersonalMessage()" id="btnSetPersonalMessage" type="button" value="Set" /> </div> </td></tr> <tr><td> <div id="sendMessage"> <hr /> <span><b>Send a Message:</b></span> <p id="contactLabel"></p> <p id="msgLastRecv"></p> <div id="txtConv"></div><br /> <input id="txtMessage" type="text"/><br /> <input onclick="sendMsg()" id="btnSend" type="button" value="Send Message" disabled="disabled" /> </div> </td></tr> <tr><td> <div id="divConversations"></div> </td></tr> <tr><td> <div id="Contacts"> <p><b>Contact List <a href="Javascript:ToggleContacts();"> <img alt="ToggleContacts" src="minus_icon.gif" id="ContactsExpand" class="ContactImage" /> </a> </b></p> <div id="divContacts"></div> </div> </td></tr> </table> </div> </form> </body> There is nothing really hard about this. We are defining various zones in which we will put various pieces of information. A zone for the sign-in frame which will actually house a iframe that links to Windows Live Identity. A zone where we will display various bits of information about the user that has signed in and allow them to change their messenger status just like you do in the Messenger client, zones for messages and for displaying your contacts. When the page loads it calls the scriptMain() Javascript function. In order to use the Windows Live Messenger API you need to include a reference to it. It's really just a big Asp.Net Ajax Javascript library. So at the top of your add the following reference :- <script src="http://settings.messenger.live.com/api/1.0/messenger.js" type="text/javascript" language="javascript"></script> Now let's take a look at the scriptMain() function :- function scriptMain() { Here we setup two variables that hold the URLs to our privacy page and our channel page as discussed earlier. You will of course need to replace "[weburl]" with your own URL. Next we create a new instance of the sign-in control. The sign-in control takes four parameters as defined here :- public SignInControl( string controlId, string privacyStatementUrl, string channelUrl, string market ) The first parameter is the zone/control in which we wish to place and display the actual sign-in control. In our case this is defined as a <div> with the ID of signinframe. The next two parameters we pass in the variables we setup to point to our privacy and channel pages and the last parameter is for localization where you pass in language information. The final statement simply adds a delegate (for those familiar with .Net programming) that calls a Javascript method authenticationCompleted when sign-in has completed. See http://msdn2.microsoft.com/en-us/library/microsoft.live.messenger.ui.signincontrol_events.aspx for further information. The authenticationCompleted event handler is similarly easy. As it's Asp.Net Ajax based events like this should look familiar to anyone who has experience coding Asp.Net web pages :- function authenticationCompleted(sender, e) { On completion of login, this event handler gets called and a user identity gets passed through as the parameter. So our first line of code in this event handler is to create an actual user object from the identity that gets passed in. Next we add another delegate that gets called once sign-in is completed for that actual user (there is a difference between the sign-in control being completed and an actual user finishing authorization hence the two different events). Finally we just call the signIn method of the user class and clear it out. There are various events that you can hook into here. For a full list, please see http://msdn2.microsoft.com/en-us/library/microsoft.live.messenger.user_events.aspx. For example you should also include a delegate to hook up when the user signs back out so that you can clear cookies, clear up your UI etc. function signInCompleted(sender, e) { if (e.get_resultCode() === Microsoft.Live.Messenger.SignInResultCode.success) { _user.get_presence().add_propertyChanged(Delegate.create(null, user_Presence_PropertyChanged));displayUserInfo(); _addressList = new Array(_user.get_contacts().get_count()); var enum1 = _user.get_contacts().getEnumerator(); while (enum1.moveNext()) { var c = enum1.get_current(); var address = c.get_currentAddress(); address.get_presence().add_propertyChanged(Delegate.create(null, presence_PropertyChanged));} displayContacts(); var selectStatus = document.getElementById('selectStatus'); selectStatus.selectedIndex = 6; _contactCollection = _user.get_contacts(); _user.get_conversations().add_propertyChanged(Delegate.create(null, conversation_collectionChanged));} } The first thing that we do in our signInCompleted event handler is to check that the sign-in process was actually successful (the user may have typed an incorrect password for example so you do want to check for this as this event handler will get called regardless of whether login was successful or not). If the user logged in to Windows Live Identity correctly then we add another delegate so that we can change the users presence or status (i.e. now show that the user is online). We call a method to display various information about the user then we cycle through the users contact list and get the presence or status information of everyone in their contact list (so that the user knows online, who's away etc) and display that information in using the displayContacts() function. The next couple of lines we simply use the dropdown list that we defined in our html and set the it to "online" and finally we setup yet another delegate, this time for actual conversations that will take place. function user_Presence_PropertyChanged(sender, e) {displayUserInfo(); if (_user.get_presence().get_status() === Microsoft.Live.Messenger.PresenceStatus.offline) {document.getElementById("divContacts").innerHTML = ""; document.getElementById("txtConv").innerHTML = ""; document.getElementById('btnSend').disabled = true; } } Here we update the users status with the call to displayUserInfo. Then we check to see if the user has actually signed out. If they have then their status will be set to offline. If they have indeed signed out then we clear out various zones on our web page like the users list of contacts. The presence status can be one of the following enumerations :- If you've followed the above code so far, you'll see that we've called the displayUserInfo function a couple of times. Lets take a look at that function :- function displayUserInfo() {var userInfo = document.getElementById('userInfo'); var userAddress = _user.get_address().get_address(); var userDispName = _user.get_presence().get_displayName(); var userPersonalMessage = _user.get_presence().get_personalMessage(); var userStatus = Enum.toString(Microsoft.Live.Messenger.PresenceStatus, _user.get_presence().get_status());var statusLine = document.createElement('p'); removeChildrenFromNode('userInfo');if (userDispName !== '') { statusLine.appendChild(document.createTextNode(userDispName + ' (' + userAddress + '): ' + userStatus)); } else { statusLine.appendChild(document.createTextNode(userAddress + ': ' + userStatus));} userInfo.appendChild(statusLine); document.getElementById('personalMessage').value = userPersonalMessage;} First we get a reference to the zone we defined on the page to display information about the user who is sign-in. In our page this is simply a <div> block. Next we collect various bits of information from the user object like the users email address, their status (online, offline, away etc.), their personal message, display name etc. We create a new paragraph element to house all this information and we clear out any information that may already be contained in our user information zone. Depending on whether the user has a display name defined or not we create an display message that will either consist of their display name, email address and status or just their email address and status. We add this information to the paragraph tag we created earlier then we add all this into our user information zone. Finally we update the users personal message (if they have one defined) separately. The reason we do this separately is that the users personal message is contained in a textbox allowing the user to update their personal message if they so desire. This function calls another function removeChildrenFromNode :- function removeChildrenFromNode(id){ var node = document.getElementById(id);if(node == undefined || node == null) { return;} var len = node.childNodes.length; while (node.hasChildNodes()) {node.removeChild(node.firstChild); } } All that this function does is to remove all the content (sub elements) that may be contained within the element that is passed in as the parameter to the function call. Going back to our signInCompleted event handler. We have now covered the first of the event handlers. The next event handler gets called whenever one of the users contacts changes their status :- function presence_PropertyChanged(sender, e) {displayContacts(); } Here we simply call another method to update the users contacts list. We use another method rather than placing all the code in this event handler as that other method will be called from various places whereas we cannot call this event handler from our code normally (this is not strictly true but this way is a lot easier). function displayContacts() {var sb = new StringBuilder(); _addressList = new Array(_user.get_contacts().get_count());var groupList = new Array(_user.get_groups().get_count()); var enum2 = _user.get_groups().getEnumerator(); var enum1 = _user.get_contacts().getEnumerator(); var groupindex = 0; while (enum2.moveNext()) { var c = enum2.get_current(); var name = c.get_name(); sb.append("<div>"); sb.append("<a href=\"javascript:ToggleContactGroup('");sb.append(name); sb.append("');\">"); sb.append("<img class=\"ContactImage\" alt=\"ToggleContacts\" src=\"minus_icon.gif\" id=\"ContactsExpand");sb.append(name); sb.append("\"/>"); sb.append("</a>");sb.append(name); sb.append('<hr />'); sb.append("<div id='");sb.append(name); sb.append("'>");var _contacts = new Array(c.get_contacts().get_count()); var enum3 = c.get_contacts().getEnumerator();index = 0; while (enum3.moveNext()) { var contact = enum3.get_current(); var address = contact.get_currentAddress(); var dispName = address.get_presence().get_displayName(); var currAddress = address.get_address(); var status = Enum.toString(Microsoft.Live.Messenger.PresenceStatus, address.get_presence().get_status()); //Change status to an imagevar statusimg = null; switch(status) {case "online": statusimg = "<img src='online.gif' class='ContactImage' alt='online' />"; break;case "offline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "appearOffline": statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;case "away": statusimg = "<img src='away.gif' class='ContactImage' alt='away' />"; break;case "beRightBack": statusimg = "<img src='away.gif' class='ContactImage' alt='beRightBack' />"; break;case "busy": statusimg = "<img src='busy.gif' class='ContactImage' alt='busy' />"; break;case "idle": statusimg = "<img src='away.gif' class='ContactImage' alt='idle' />"; break;case "inACall": statusimg = "<img src='busy.gif' class='ContactImage' alt='InACall' />"; break;case "outToLunch": statusimg = "<img src='away.gif' class='ContactImage' alt='OutToLunch' />"; break; default: statusimg = "<img src='offline.gif' class='ContactImage' alt='offline' />"; break;} var statusLine = ''; var strDelete = ''; _addressList[index] = address; if (dispName !== '') { statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + dispName + '</a>'; } else {statusLine = '<a href=\'javascript:createConv(' + index + ')\'>' + statusimg + currAddress + '</a>'; } sb.append(statusLine); sb.append('<hr />');index++; } sb.append("</div></div>");groupindex++; } document.getElementById('divContacts').innerHTML = sb.toString();} Now this method may look fairly daunting but really it's not. First we get create a new StringBuilder object (part of the Asp.Net Ajax framework). Then we get a list of all the users contacts. Next we also get a list of all the users groups. In the Windows Live Messenger client you can place your contacts in various groups (and even create your own groups) for example co-workers, friends, family etc. Both of these lists are collections. What we want to do is to display each contact in the correct group. So first we create a loop to loop through all the groups. var c = enum2.get_current();var name = c.get_name();Within the first loop (group loop) we've hard-coded some html that will allow a user to close and expand a group (otherwise the page may get very long if the user has a lot of contacts). Here we're just defining a couple of zones (<div> elements) to put the information in. The outer zone will hold the group name and an icon to display whether the zone is collapsed or expanded. The inner zone will hold all the contacts for that group and will be the zone that actually gets collapsed or expanded. Next we get a list of all the contacts that belong to that particular group :- var _contacts = new Array(c.get_contacts().get_count()); And we define a second inner loop (contact loop). In the contact loop we get each contact, extract various pieces of information from it like their email address, current status, display name etc. ready for display. Next, based on the contacts status, we insert the appropriate messenger presence icon. We do this by simply checking their status against the values of the presence enumeration (see above) and hard coding the appropriate image element against the status. Finally we construct what it is that we will actually be displaying to the user. If the contact has a display name then we display that along with the appropriate status icon, if not then we display the contacts email address along with the appropriate status icon. These we make into links that call the function createConv() so that we can start a conversation with that contact simply by clicking on their information, much the same as the Windows Live Messenger client does. At this point we are about half way through the Javascript code necessary to create Windows Live Messenger within your own web pages. In part two of this article we will finish off our discussion with the remaining Javascript which covers conversations. February 26 Microsoft's lack of sticking to standards rears it's ugly head againI was going to do a short article on how easy it is to extract information and generally mess around with Windows Live Spaces blog posts in your code using LINQ, however due to Microsoft's lack of sticking to standards, this has become impossible for what I was intending to write the article about. You still can do some things with Windows Live Spaces blog posts and LINQ but unfortunately just not the really interesting stuff. Let me explain. You can access a Windows Live Spaces blog post through the RSS feed that Windows Live Spaces (WLS) exposes, for example "http://msnwindowslive.spaces.live.com/feed.rss". This is the main feed for site. You can even select more specific posts by category, for example "http://msnwindowslive.spaces.live.com/category/Programming/feed.rss". Simply insert "category/[category name]" after the main url followed of course by "feed.rss". Since an RSS feed is really only an XML document this can easily be loaded into an XElement or XDocument in LINQ :- using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Xml;using System.Xml.Linq;namespace LINQTest{class Program {static void Main(string[] args) {XElement xel = XElement.Load("http://msnwindowslive.spaces.live.com/feed.rss"); } } } Here we are simply loading the RSS feed into an XElement object. Once you have the RSS feed you can then set about querying it. As a quick example here is some code displays all the blog post titles from a WLS RSS feed :- using System;using System.Collections.Generic;using System.Linq;using System.Text;
using System.Xml;using System.Xml.Linq;namespace LINQTest{class Program {static void Main(string[] args) {XElement xel = XElement.Load("http://msnwindowslive.spaces.live.com/feed.rss"); IEnumerable<XElement> titles = xel.Descendants("item").Descendants("title"); foreach (XElement title in titles) { Console.WriteLine(title.Value.ToString());} Console.ReadLine();} } } As you can see, extremely easy. First we load the RSS into an XElement object as described above. Since there may be more than one blog post title in the feed, we can't simply map it to a single XElement object therefore we create a collection of XElement objects using IEnumerable<XElement>. We call this collection titles and we assign all the Title elements that are sub-elements of Item to this collection. For those who are unfamiliar with RSS feeds, each blog post is an "Item". Within each "Item" there are numerous tags including "Title" which is the title of blog post. If you want the actual article itself you would simply swap "title" for "description" as the actual blog post is contained within the description element of each item. Now this is where things were going to get interesting and the meat of article would come from. A blog post in WLS is really only an XHTML snippet . XHTML as you can probably tell from the "X" is really just an XML document. If it's just an XML document then we should be able to load the actual blog post into an XElement and start playing about it, for example, extract all the images contained within the blog post, amend the XHTML etc. Unfortunately because the XHTML is non-standard we cannot do this. When you try to parse the XHTML into an XElement object, you will receive a runtime error. On first inspection this is because all the angled brackets have been escaped (html encoded). I can definitely see why Microsoft done this and this is very easy to fix, simple replace the escaped character sequence with the actual angled bracket and then parse this string into an XElement object :- using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Xml;using System.Xml.Linq;namespace LINQTest{class Program {static void Main(string[] args) {XElement xel = XElement.Load("http://msnwindowslive.spaces.live.com/feed.rss"); IEnumerable<XElement> x = xel.Descendants("item"); foreach (XElement el in x.Descendants("description")) {string desc = (string)el.ToString(); desc = desc.Replace("<", "<"); desc = desc.Replace(">", ">"); XElement els = XElement.Parse(desc); IEnumerable<XElement> imgs = els.Descendants("img"); foreach (XElement img in imgs) { string imag = img.ToString();} } Console.ReadLine();} } } Here we are simply looping through all the blog posts, changing the escaped angled brackets back to what they should be and then parsing that into an XElement. Unfortunately when you run this code you get an error. In this example the error is caused by the following :- <a href="http://technorati.com/tags/AJAX" rel=tag>AJAX</a> Can you spot why this line would through up an error? This is given because the XHTML that Microsoft uses in WLS is non-standard. There is a mixture of quoted and unquoted attributes. According to official specs, all attributes should be quoted and the "rel=tag" attribute is not quoted. Upon noticing this I went through the XHTML that WLS puts out and there numerous examples of Microsoft is not playing by the rules. Now when parsing this into LINQ, LINQ is expecting all attributes to be quoted, since this one is not it doesn't know what it is and throws the error. All the example blog posts that I was taking RSS feeds from were created using Windows Live writer. Therefore I think that it's actually Windows Live writer that spits out non-standard XHTML and not the fault of WLS which really is just the display container, although I would be surprised if the editor within WLS itself actually gives standards compliant XHTML. So there you have it. What could have been a fairly interesting article showing you how to play around with WLS blog posts in your own code using LINQ, cut short by Microsoft's non-standards compliance. January 10 More Pictures - An Experiment
This is basically an experiment linking one of my own websites running a Silverlight slideshow through my spaces page. January 03 InfoPath - How to calculate number of days offAnother little scenario given to me by my boss to work. We're working on putting our holiday/leave request forms online. Since we have a Sharepoint deployment, my boss thought it would be easy to simply put together an InfoPath online form for people to fill out to request days off using Forms Services/Forms Server. However there was thing that we needed to do but InfoPath doesn't allow you to do using the built in functions and that is calculate the actual number of days taken off. In Visual Basic this should as simple as using the DateDiff() function passing in the start and end dates of the holiday. Unfortunately C# doesn't have the DateDiff() function. Another problem is what if people are taking off a period of time that spans an actual holiday (for example, someone requests the first week of this year off. This would be December 31st 2007 through to Sunday 6th Jan 2008, returning to work Monday 7th 2008. Since we get Tuesday 1st Jan off as an actual holiday then we shouldn't calculate this into our results (actual number of vacation days taken in this instance would only be 4)). One other problem to overcome is that the date request section of the form should be repeating to allow the person to enter multiple requests at one time. The task I got given was to come up with a proof of concept InfoPath form that would do all this. So let's get started. Open Infopath and design a new form from the blank template. In the design tasks pane select controls and drag out a Repeating Table onto your blank canvas. Give it the default 3 columns. The first column will be the start date of the request, the second column will be the end date and the third column will be the actual number of days vacation time taken. In the Design Tasks pane, come out of Controls and go into Data Source view. Give the 3 fields proper names rather than just Field1, Field2 etc. Click on the arrow next to the field and select properties then change it's data type to be Date rather than string and check the Cannot be Blank box. Hit OK to come out of that then click on the arrow again and change the display type to be a Date Picker. Do this for the second field as well. Just leave the third field as a TextBox with a data type of string. You should end up with something similar to this :- Now right click on the first date picker and select Progamming/Changed event. This should open up Visual Studio IDE with the appropriate event already mapped out for you. In this event handler we want to make sure that the operation was infact a change in the fields value, also make sure that the end date field is not null (you cannot calculate the number of days given only one date) and if so then call our CalculateDaysOff method. The last thing we want to do is to insert the result from the CalculateDaysOff back into the repeating table in the third data field. public void StartDate_Changed(object sender, XmlEventArgs e) { if (e.Operation == XmlOperation.ValueChange) { DateTime EndDate; //Make sure the enddate isn't null; if(DateTime.TryParse(e.Site.SelectSingleNode("../my:EndDate", NamespaceManager).Value, out EndDate)) { string DaysOff = string.Empty; DaysOff = CalculateDaysOff(e.Site.SelectSingleNode("../my:StartDate", NamespaceManager).ValueAsDateTime, e.Site.SelectSingleNode("../my:EndDate", NamespaceManager).ValueAsDateTime); XPathNavigator result = e.Site.SelectSingleNode("../my:Result", NamespaceManager); result.SetValue(DaysOff); } } } Our CalculateDaysOff method will take the start and end dates as input parameters and eventually return a string indicating the actual number of days taken off. Now as you'll remember from the scenario given above, we also have to factor in whether a date is a weekday or a weekend (we work Monday through Friday and get weekends off) and also whether it is an actual holiday or not. For the holidays, there's really no other way around this but to store the actual holiday dates in a database (for this you just need a single table with one field to store the holiday dates in but I've given the table two fields) :- Next we need a Stored Procedure to call that simply returns all the dates stored in the table :- USE [InfoPathHolidays] GO /****** Object: StoredProcedure [dbo].[AllHolidaysGet] Script Date: 01/03/2008 14:14:12 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ============================================= -- Author: <Author,,Name> -- Create date: <Create Date,,> -- Description: <Description,,> -- ============================================= ALTER PROCEDURE [dbo].[AllHolidaysGet] AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT ON; -- Insert statements for procedure here SELECT HolidayDate FROM dbo.Holidays ORDER BY HolidayDate; END Now fill in some dates and that's all we really need from the database (you can give a unique login to this using SQL Authentication rather than windows authentication and use that in your code if you'd like as well). OK, back to our InfoPath project. The first thing we want to do in our CalculateDaysOff method is to retrieve this table of holiday dates from the database :- private string CalculateDaysOff(DateTime StartDate, DateTime EndDate) { DataTable dt = GetHolidays(); Since it's only a single table that's being returned I've separated this call out to another method that returns a Data Table containing the results rather than a Data Set. All this method does is call the stored procedure from our database, fill a Data Set using the Data Adapter then select the first table and return this to our CalculateDaysOff method :- private DataTable GetHolidays() { //Reason the SQL commands to retrieve the dataset are here rather //than in the IsHoliday function is so that we only need to //Retrieve the dataset Once rather than retrieving it //For every day string connString = "Database=InfoPathHolidays;Server=ADG-CEC322A;uid=InfoPathHoliday;Password=InfoPath;"; SqlConnection conn = new SqlConnection(connString); SqlCommand cmdSQL = new SqlCommand("AllHolidaysGet", conn); cmdSQL.CommandType = CommandType.StoredProcedure; SqlDataAdapter da = new SqlDataAdapter(cmdSQL); DataSet ds = new DataSet(); DataTable dt; try { conn.Open(); da.Fill(ds); conn.Close(); } catch { throw; } dt = ds.Tables[0]; ds.Dispose(); return dt; } Simple. Now that we have our Data Table containing the dates of actual holidays we can finish off our CalculateDaysOff method :- TimeSpan Difference; int DaysOff = 0; int WeekendDays = 0; int Holidays = 0; Difference = EndDate - StartDate; DaysOff = Difference.Days; for (int i = 0; i < DaysOff; i++) { DateTime LoopDate = StartDate.AddDays(i); if (IsWeekend(LoopDate)) { WeekendDays++; } if(IsHoliday(LoopDate, dt)) { Holidays++; } } dt.Dispose(); DaysOff -= WeekendDays; DaysOff -= Holidays; return DaysOff.ToString(); } Here need to check whether a date is a weekend and also whether it's a actual holiday so I've set up two separate counters, one for each, then we simply loop through the days from start to end and check to see whether it's a holiday or a weekend and increment the appropriate counter if it is. Finally we subtract the number of weekend days and holiday days within our date ranges and that's the result that will be passed back from this method and displayed back in our third field in the repeating table. To calculate whether the date is a week day or a weekend day we simply check the Day of week against Saturday and Sunday and if it's not either then it's a weekday and our IsWeekend function will return false, otherwise it is a weekend day and our method will return true :- private bool IsWeekend(DateTime date) { if (date.DayOfWeek != DayOfWeek.Sunday && date.DayOfWeek != DayOfWeek.Saturday) { return false; } else { return true; } } Next we basically do the same sort of thing to check whether the date within our loop is a holiday or not. We do this by looping through the Data Table we retrieved earlier from the database and comparing the date against each value in the table. If the date matches we return true that it is a holiday date, otherwise false :- private bool IsHoliday(DateTime date, DataTable dt) { for (int i = 0; i < dt.Rows.Count - 1; i++) { DataRow dr = dt.Rows[i]; DateTime HolidayDate = Convert.ToDateTime(dr["HolidayDate"].ToString()); if (HolidayDate == date) { return true; } } return false; } And that's basically it. You can also hook up the second field (end date) in the repeating table to an event handler and call the same routines from it so no matter if the end user changes the start date or end date, the third column in our repeating table will always calculate :- public void EndDate_Changed(object sender, XmlEventArgs e) { if (e.Operation == XmlOperation.ValueChange) { string DaysOff = string.Empty; DaysOff = CalculateDaysOff(e.Site.SelectSingleNode("../my:StartDate", NamespaceManager).ValueAsDateTime, e.Site.SelectSingleNode("../my:EndDate", NamespaceManager).ValueAsDateTime); XPathNavigator result = e.Site.SelectSingleNode("../my:Result", NamespaceManager); result.SetValue(DaysOff); } } And what you should end up with is our repeating table that automatically (and correctly) calculates that actual number of vacation days to be deducted from a person when they request leave :- December 20 The Learning CurveI've just read the editors intro by Rod Paddock to this issue of Code Magazine and it got me thinking about how very true to a certain point it is. It was talking about specialists and generalists (you can read the article here). Now I am a generalist in some senses and a specialist in others, I need to know about a lot of technologies, how they interoperate, which ones are best for which situations etc. but at the same time I need to know about these technologies in-depth as I am the one my colleagues and friends come to to learn, ask questions etc. So in essence Rod's article is correct however in some senses it is incorrect as some people fall into both categories. One of my main roles is that of a web developer, which I have been doing since the very early days of the web (think Mosiac and Marc A.'s first web browser although back then you couldn't so much call it web development). On top of that I spend a lot of time developing for WSS and Sharepoint 2007 (MOSS), a SQL Server DBA (to an extent) and also a Systems Architect and integrator (How many servers are required for a Sharepoint Farm that uses Microsoft's ISA Server, Load balancing, clustering of SQL Nodes, what's the best deployment scenario for scalability etc. and have to install and configure all these servers, setup Kerberos authentication etc.). Now given all this, the range of technologies and languages that you have to learn is quite frankly enormous and I've noticed that I'm actually getting left slightly behind in the race due to the sheer number involved, especially with how in-depth I need to know each one. As an example, just take one role as a Web Developer. In that role (of course I'm leaning here towards a Microsoft platform based web developer), I need to have an in-depth understanding of ASP.Net, HTML, DHTML, XHTML, JavaScript, CSS, C#, VB.Net, XML, XSD, XSLT, XPath, Microsoft's .Net Framework etc. etc. Now quite a few of the web developers on my team know bits from each of these technologies, some know more about some of the technologies and languages than others and others may not know some of the technologies or languages at all. I by contrast have to know every single one of them in-depth. What do I mean by in-depth? Well I have to know how to program in them, what the W3C recommendations are for them, best practices (example recommended way to layout a page use DIVs, relative and absolute positioning vs tables, CSS guidelines etc.), cross browser issues and how to resolve them. The list goes on and on and that's really only for basic XHTML/HTML/CSS, never mind the host of other technologies that I have to know about. On top of all this, the pace at which things change is tremendous and if you don't watch out you'll soon be left behind in the dust. A great example is this Web 2.0 stuff. Basically AJAX, mash-ups, personalized content etc. Although AJAX etc. has been around for a while (introduced by Microsoft back in 1998) it has really only caught on in the last year or so. So on top of everything I need to keep myself up to date with things. One way I do this is by reading, and I mean reading a lot. To this end I have found myself reviewing books for where I work, make recommendations to friends and also to other IT professionals through the User Group that I run. I have received great feedback from people for books that I recommended, purely because I read so many of them in order to learn all these technologies and languages myself. I also review software and make recommendations on software for all the above as well. Again this is partly based on my constant need to try to keep up to date with things, but also to try and my and my colleagues work a lot easier. This however also takes time. It takes time to learn a new product, how best to use it, sometimes even how to install it and best practices (Team Foundation Server 2005 is a great example of that). Another great example is third party controls like those from Telerik, DevExpress, Infragistics etc. You need to learn how these controls work, how to customize them, how to program against their API's, which control is best for which situations (example, all of the above mentioned companies offer some form of grid control). Again I have received thanks from various people and companies for my recommendations of various software and third party add-on products because I know them (as you may tell I like to help people as well, one of the reasons that I am and have been a Microsoft MVP for a number of years now). Now whilst taking all of the above into consideration I have noticed something and that is the pace at which Microsoft is releasing new products. I have to keep my knowledge cutting edge and try to use leading edge technologies in the software that I develop (if it makes sense of course) but I've noticed that in reality we're still actually developing for and learning nuances to Microsoft's .Net Framework 2.0. I've spoken with friends who work in other companies and they are the same. The predominant platform at the moment appears to .Net 2.0. Now as I'm sure you'll know, since then Microsoft has had 2 major updates to .Net 3.0 and again recently to .Net 3.5. and here is where I think Microsoft may have a problem. The .Net Framework is absolutely huge. If there's something that you need to do then chances are there's a class or method somewhere in there that will do what you need. It is so big that I very much doubt any one person (and yes I do think Anders H. falls into this category as well) actually knows it all in-depth. .Net 2.0 brought a lot of changes and new technologies and took a while to learn. I think a lot of people are just now getting to a stage where they know enough about it to be really say that they're good at using it. In the meantime however, as mentioned, Microsoft has released 2 updates to it. It is a fine line that Microsoft is facing, they need to keep everything up to date with all the latest things that are happening in the world outside (and inside) of Microsoft, but at the same time they really can't put out too many releases in fairly rapid succession as people will just get left behind. You also need to take into account that most companies do not move as fast as the industry in general does, hence why most (and by most I actually mean virtually all) of the people I spoke to are still using .Net 2.0 and haven't progressed beyond that. By the time they start learning about WF, WCF, WPF etc. (.Net 3.0), Microsoft will have released .Net 4.0 and probably be half way through their release cycle to .Net 4.5. Am I the only one that thinks this? How many other people out there need to know as many languages and technologies as I do? Are you a specialist, a generalist or both? Do you think Microsoft's release cycle is too short? Just a few things to ponder. December 18 Tafiti Search Visualization - OverviewIf you head over to Codeplex you will find a number of Contoso Quick Applications posted there. Each one demonstrating various aspects of the Windows Live platform and showing you a "real world" style application that makes use of them, along with the source code so that you can easily apply the various techniques to your own web site. Today another web application will has been released. The focus of this application is Silverlight and Windows Live Search. This short article will give you a quick overview of it's main features without doing a "deep dive" into exactly how it works. The Context Most people that I've known can relate to this. You come across a problem and you need to do some online research, say for example, how to integrate Windows Live ID in with your web site. So you hit the Internet and start doing some searches, you find bits of information of one web site, some bits on one blog post, some bits on another. Then you want to find some images for a login and logout button. So you've got all these web sites that you need to remember where the information is and you possibly may also need to share you're research will a fellow worker that's working on the project with you. The Deployment GuideAngus Logan and the crew from SharpLogic have taken a lot of time and effort and created an excellent deployment guide for this Quick Application. Setting it up is slightly more complex than previous Windows Live Quick App's however the Word document (2007 docx format) included with the download gives you step by step instructions on how to setup and get Tahiti working, including numerous screen shots where applicable. Overall, including registering the application with the search and Windows LiveID sites, setup shouldn't take you more than 5 minutes. The ApplicationWhen you initially run the application, you will be greeted by a simple search box, in the top left of the application are various links to terms and conditions etc. for this sample application along with a link allowing you to authenticate through Windows LiveID. So first we sign-in to the application :- After logging in you are taken back to the initial search box. Simply enter any search term you like in the search box. This allows for the normal search parameters such as searching for exact phrases surrounded by quotes and exact matches using the "+" sign etc. After you've entered a search term you'll be presented with a screen like above. The main portion of the screen is taken up with the results of your search, the search box has moved over to the left should you want to change your search term. Just below this is an options wheel which I'll get to in a minute and over on the right is a list of your saved results. The default search is a web search. At the top of the search pane you can filter the results on based on another search query, for example, in the main search query I searched on the term "Liveside", I then filtered the list of results returned on "Angus" and the result was a single article/link :- The filtered search only filters the results that have been returned and are displayed on the page therefore it is very quick to filter. The Options WheelOn the left hand side, just below the search box you'll see the options wheel.
Each search term that you enter is saved and is represented on screen by a stack of cards :- To see a previous search, simply close (Red X in upper left corner) the currently displayed card and the previous search term will appear and the output window will change to reflect this. Save your search resultsOn the right hand side of the page, you have 5 "Buckets" in which you can save individual results. For example, if you do a search on Liveside and come across a link you like then simply drag it from the main window over to one of the buckets. The buckets will accept any of the search results, be it a link to a web page from the main web search function, an RSS feed, a picture etc. You can save multiple entries in each bucket, they essentially get stacked and can also label each bucket as a quick reminder. If you have signed in using Windows LiveID, these buckets get saved so that the next time you open the application, your saved searches will re-appear waiting on you. From here you can either email your saved to results to someone or post the results to your Windows Live Spaces blog site. Tree ViewIf you wish something a little more visually exciting for your search results you can select the Tree View link at the top of the main search results list. The result of which is that your search results are displayed on a slowly rotating tree. This only works for standard Web searches and not for any of the other options in the Options Wheel. What's Next?The Tafiti Quick Application shows you some best practices and examples of how to integrate Windows Live Search and Windows LiveID with your Silverlight application. Next we will be taking a more in-depth look at this application and breaking down in a series of "deep dive" articles, so stay tuned. December 05 Add Silverlight to Sharepoint 2007Recently I was asked how to display Rich media (mainly video) in Sharepoint 2007 by our communications department. This shouldn't be too hard to do so time to whip up a quick demo/proof of concept to make sure that it in fact can be done, and what better way than to create a custom web part that embeds Microsoft's Silverlight. Adding Silverlight to your web application is easy enough so it shouldn't be too difficult to do in Sharepoint 2007 either. There are a few different files that you will need in order to do this however, a couple of Javascript files, a XAML and of course your custom web part. So kick off Visual Studio and lets get cracking. First off we'll create the Javascript file that you will need (the other one you don't create, just copy from the SDK but more on that later). I'm assuming that you have Sharepoint extensions for Visual Studio installed (currently available only for 2005 although a version for Visual Studio 2008 is apparently due out shortly). Go to File/New and select New Project from the list of available options. Next within the C# Project Type folder, go to the Sharepoint folder and select Web Part. Call it SilverLightPart, give it a location and hit OK. This will create the outline of the custom web part we'll create later. Next right click on SilverLightPart project and select Add/New Item. In the dialog that pops up, click on the Visual C# Project Items under categories and select JScript for the Javascript file we'll create and call it CreateSilverLight.js. The Javascript function that we'll create is a small function that simply kicks off the main Silverlight Javascript routine. Copy the following code into your blank Javascript file :- function createSilverlight() The code is commented so it's fairly self explanatory. Here we are instantiating the actual Silverlight object. This takes a number of parameters. The first parameter is where the Silverlight object can find the associated XAML file that it requires, next is a reference to a DIV tag that Silverlight will be embedded in, the third parameter is a Unique ID for the plugin followed by a number of parameters giving the width, height, framerate of the video etc. etc. In the above code, the only parameter that I am effectively passing in is the parentElement parameter, however in your own code you may want to pass in parameters for the XAML file, the width and height properties and a Unique ID. And that's all there is to this file. Next we want to create the XAML file that this script references, so add an XML file to your project and call it VideoPlayer.XAML. Delete the default <xml> tag that has been created and insert the following :- <Canvas </Canvas> Here we are defining the root element to be a canvas (just like the canvas painters use to paint on, a blank area with potential) and pointing to the relevant xml namespaces required for XAML files. Next we create our actual Canvas in which we will display our video file. The video file is marked up in XAML using the <MediaElement> tag and here we simply point it to a video that we wish to display and give it a width and height (width and height are optional but if you do input these they are in pixels. You cannot use percentages e.g. 100%). Since this is just a proof of concept we'll end things here. You can of course add whatever you like to the XAML file, video controls for play, pause etc. and hook into Javascript events that will deal with them, skin it to give it your own personal touch, overlay controls or text etc. For more information on how to do these things you may want to take a look at the excellent Quick Start videos that are freely available on MSDN at the moment. The last Javascript file that you will need is the actual Silverlight javascript file itself. In order to get this file you will need to download a copy of the SDK found here. OK so that is the ancillary files out of the way, next we start on the web part itself. I'm not going to do anything fancy here, simply display the Silverlight control which has the video file hard coded within it, however you may want to create your own web part editor and add inputs so that an end user could select the video source, change the width and height etc. using System; using Microsoft.SharePoint; [assembly: AllowPartiallyTrustedCallers] protected override void Render(HtmlTextWriter writer) protected override void OnPreRender(EventArgs e) First we've added a couple of extra namespaces and the Assembly reference to allow for partially trusted callers. This is a security setting used within Sharepoint. Each web part has to have it's own GUID but this is already supplied for you by the template (as well as the .snk file for signing your DLL when you build it amongst other things). Next we have the constructor. As this is essentially an ASP.Net 2.0 web part and not specifically a Sharepoint web part, this setting allows you to export this web part so that you can use it in other sites. This isn't actually required for a Sharepoint web part and you can feel free to delete ExportMode line, however you do require a constructor, even if it's blank. Next we have the Render method. The contents in here is actually what will be written to the page. As this was just a proof of concept, I hard coded these lines, however if you were actually building this web part for deployment you might want to use RenderBeginTag, RenderEndTag and AddAttribute methods instead. Finally we override the OnPreRender event. We have to add our two Javascript files to the page. Here we're simply saying that if these Javascript files aren't contained within the page, then add the references to them. And that's it. Build your project and next we'll deploy it. First things first, we want a place that we can add the Javascript and XAML files to so that they can be references no matter where you deploy the web part within the site. As you may have picked up from the source above, I've chosen the _Layout directory as this is globally accessible. Nearly done, next we have to ammend the web.config file to tell Sharepoint that our web control is safe to use. So open the web.config file for your site in either Visual Studio or notepad and add this to the end of the list of SafeControls already defined there :- <SafeControl Assembly="SilverLightPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5" Namespace="SilverLightPart" TypeName="*" Safe="True" /> As we've changed the web.config file it's always best to do an IISReset so that Sharepoint will immediately pick up the new config file. All that is left is to add your new Silverlight video streaming web part to your page. Navigate to the Sharepoint page where you wish to add this web part, select Edit Page from under the Site Actions menu, click on one of the Add Web Part links that are on the page and add the SilverlightPart web part to your page (found under Miscellaneous). And there you have it, how to add Silverlight to your Sharepoint 2007 site. November 18 Infragistics WebChart control problem and solution
However on playing with this control I came across a problem that I suspect a lot of people may come across. For the site that I was designing, I would use this control in a few different pages, therefore it would make sense to wrap this control within a user control (ascx), expose a few properties in the user control then I could just include the user control in any page that I need. As you can see from the structure above, I am putting all my user controls into a directory under the root. My default.aspx is contained in the root of the site and I have another page, Biography.aspx, contained in another directory, Biography, again off the root. This could be a very typical layout for many websites, you group things together into directories. Now I will be using the WebChart control from both the default.aspx page in the root of the site and also from the Biography.aspx page under the Biography folder. This is where the problem comes in. When you use the Infragistics WebChart control, you have a choice of either the control rendering it's chart to the file system or rendering it in session state. For the most part you would want it to render to the file system as some of these charts may get large in size and would eat up a lot of the web servers memory. So when you set the property of the WebChart control to render to the file system, the control looks for a folder "ChartImages", in which to store the rendered charts for display on your site. You tell the WebChart control where this directory resides by use of the DeploymentScenario tag as follows :- <DeploymentScenario Scenario="FileSystem" ImageType="Png"></DeploymentScenario> DeploymentScenario also has two additional properties, ImageUrl and FilePath. Both of these should point to the ChartImages folder and both of these take a relative URL. This is where the problem comes in. Because I'm referencing the user control from different pages in different folders, the path to the ChartImages folder is going to different for each. So how do you get around this problem? My first guess was to assign the ImageUrl and FilePath's from code behind and use Server.MapPath to give them absolute addresses that would resolve in relation to the site. Unfortunately the problem here is that they are absolute addresses and the control expects relative addresses and therefore when you try to run the site you get an error. Next I thought about using the standard tilde notation as in "~/UserControls/ChartImages" which should give the correct path relative to the root of the site. Unfortunately the WebChart control does not support the tilde notation. Next I thought, OK the ChartImages folder is within the UserControls folder where my user control resides. Therefore I should be able to just say "UserControls/" as the relative path as it is the relative path from the control itself. Unfortunately this does not work as the path is relative to the page that is getting rendered, not the user control itself. In other words, from the Default.aspx page the path would be "ChartImages/", but from the Biography.aspx page the path would actually be "Biography/ChartImages/". This causes an error when the control is rendering stating that it cannot find the relative path to ChartImages folder. The actual way of accomplishing this task is to do it from code behind and use the ResolveClientUrl function as such :- StockChart.DeploymentScenario.ImageURL = ResolveClientUrl("ChartImages/Chart_#SEQNUM(100).png"); StockChart.DeploymentScenario.FilePath = ResolveClientUrl("ChartImages/"); ResolveClientUrl correctly resolves the path to the ChartImages folder no matter where you're calling page is located. It's worth taking a look at this command if you're not familiar with it as it comes in very handy in a number of scenarios where you have to use relative addresses. Whilst you're at it, also take a look at the Infragistics WebChart control if you need any kind of charting, graphing or guage control, it's very good and flexible. November 07 Windows Live Quick Apps – Contoso Bicycle Club Part 3Welcome back to the second part of our deep dive in the Windows Live Quick Apps Website featuring the Contoso Bicycle Club. In this part we’ll take apart one of the main JavaScript function calls that this web site uses, updatePage. In order to display the main panel you have a few of options. You can click on any of the links in the Events user control, any of the links in the Rides user control or use the links from the Rides or Events dropdown menu. All of these links make a call to a javascript function updatePage that we’ve touched on in previous parts of this deep dive. Lets take a closer look at this function to see exactly what it does :- function updatePage(title, feed, item, cid, album) { hideRideMenu(); hideEventsMenu(); hideDiv($('SlideShowPanel')); hideDiv($('DirectionsPanel')); hideDiv( $('MainPanel')); hideVideo(); showDiv($('TextPanel'), 500); showDiv($('MenuPanel'), 500); showDiv($('MapPanel'), 99999); CID = cid; var titleDiv = $('Title'); titleDiv.innerHTML = title; ALBUM = album; updateText(feed, item); // Load the collection loadCollection(cid); if(album == "") disableAnchor($('PhotosFromRideLink'), true); else { if($('PhotosFromRideLink').disabled) disableAnchor($('PhotosFromRideLink'), false); } if(cid == "") { disableAnchor($('MapLink'), true); disableAnchor($('DirectionsLink'), true); disableAnchor($('ViewRouteLink'), true); disableAnchor($('BikeCamLink'), true); } else { if($('MapLink').disabled) disableAnchor($('MapLink'), false); if($('DirectionsLink').disabled) disableAnchor($('DirectionsLink'), false); if($('ViewRouteLink').disabled) disableAnchor($('ViewRouteLink'), false); if($('BikeCamLink').disabled) disableAnchor($('BikeCamLink'), false); } // Hard coded link for London Bike Cam demo switch(title.toLowerCase()) { case "london river thames": if($('BikeCamLink').disabled) disableAnchor($('BikeCamLink'), false); break; default: disableAnchor($('BikeCamLink'), true); break; } } The first few function calls simply cleans up the screen a bit should it need to. The first two function calls hide the divs that contain the drop down menus for Rides and Events found at the top of the screen. The next three statements also hide regions defined in the default.aspx for for display of various media. If you’re not used to Asp.Net Ajax, the function call may look a little strange as it uses some shorthand notation :- hideDiv($('DirectionsPanel')); The $(‘element name’) notation is just shorthand for document.getElementById(‘element name’); The next function call hides the video panel should it be open :- function hideVideo() { var div = $('VideoWrapper'); if (div.style.visibility == 'visible') { document.getElementById('VideoControl').content.findName('VideoWindow').pause(); PAUSED = true; hideDiv(div); deleteCyclist(); } } As you can see, if the panel is displayed, first off it pauses the video, then hides the panel. Finally it calls a function that deletes the cyclist icon from the map. The cyclist icon is in reality just a custom pushpin. We’ll take a closer look at this later. Back to the updatePage function. Next we put in some calls to display various panels, the main map panel, a text panel that will hold the blog description and a menu panel giving the user some options to choose from. Next we set the Title of the main portion of the page to the Title of the blog (see the previous article in this deep dive serious for an explanation of the input parameters to the updatePage function call). The next function call that we make displays the description from our blog into the text panel that we’ve just displayed :- function updateText(feed, item) { var textPanel = $('TextPanel'); loadHTML( 'Item.aspx?feed=' + feed + '&item=' + item, textPanel) } As you may recall, the parameter feed is the URL to the RSS feed of the blog post and item is a counter to basically give each post its own ID. In this context it tells the updateText routine which blog post we want from the RSS feed. The first statement simply gets a reference to the TextPanel div itself. The next function call is slightly more involved. It places a call to the loadHTML function which is found in the ajah.js file in the /js subdirectory of the web site :- function loadHTML( url, div) { var xmlHttpRequest = createXMLHttpRequest(); var handler = function() { if (xmlHttpRequest.readyState==4) { if (xmlHttpRequest.status==200) { div.innerHTML = xmlHttpRequest.responseText; } else { alert('Error - LoadHTML failed.'); } } } xmlHttpRequest.onreadystatechange = handler; xmlHttpRequest.open("GET", url, true); xmlHttpRequest.send(null); } We pass in the url of the html we wish to access and the panel that it should be displayed in. First off here we create an xmlHttpRequest option. Due to different browsers handling this in different ways a function has been written to get the appropriate xmlHttpRequest option for the browser that the user is currently using :- function createXMLHttpRequest() { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {} try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {} try { return new XMLHttpRequest(); } catch (e) {} alert("This browser does not support Ajax"); return null; } Next we create a prototype javascript function that will be called whenever the xmlHttpRequest object changes it’s state. As you no doubt know, the xmlHttpRequest object allows you to place out of band asynchronous server side calls. Because of the asynchronous nature of it, you have to give it a method that it returns (think of this like a delegate in standard .Net development). What we’re saying here is that every time the state has changed in the ajax call, return to and run the handler prototype that we’ve setup. The readyState can have one of 5 values :-
All the data has been received, and is available. What we’re interested in is when readystate returns the value 4, which basically means the ajax call has completed. Next we check that status value, this is basically the standard html status codes :-
As you can see, we’re checking for the value 200 which means that everything is OK and no errors were encountered. Finally in our prototype we fill the div (panel) that we passed into the main function with the value returned by our ajax call. The final three lines of the main function call just place the ajax call. It is interesting to note here that this is a manual Ajax call and it doesn’t use the Asp.Net Ajax framework to place the call by using the createCallback and createDelegate functions built into Asp.Net Ajax. All of the above, as mentioned, takes our blog entry and pastes the html into the text panel that we showed on the main page. Back in our updatePage method, the next call we place is to load the map collection that we previously stored (see prevous article for explanation of map collections). // Load the collection loadCollection(cid); We pass across the collection id that is unique to our map collection and passed into the updatePage method. function loadCollection(cid) { // Delete any pre existing shapes from the layer COLLECTION_LAYER.DeleteAllShapes(); // Create the new layer with the Collection var veLayerSpec = new VEShapeSourceSpecification(VEDataType.VECollection, cid, COLLECTION_LAYER); // Import the layer MAP.ImportShapeLayerData(veLayerSpec, onFeedLoad, true); } The first statement uses a virtual earth collection layer. This is defined further up in the javascript :- // The layer used for the maps.live.com collection var COLLECTION_LAYER = new VEShapeLayer(); You can think of a VEShapeLayer as a layer in a paint program. It allows you to draw on it without affecting the original underneath. In the case of Virtual Earth maps, it allows you to add polylines, polygons and pushpins to the underlying map. The first thing we do here however is call one of the built in methods to erase any shapes that may have been previously created on this layer. Next we define the layer to hold the map collection that we want. We’re telling it that we want collection, the unique id of the collection that we want to define as a layer and the actual layer itself to which this data should be added. Finally we merge our map collection data layer with the map itself. Once the data has been retrieved and merged we make a call to the onFeedLoad function :- function onFeedLoad(feed) { var count = COLLECTION_LAYER.GetShapeCount(); for(var i=0; i < count; ++i) { var shape = COLLECTION_LAYER.GetShapeByIndex(i); shape.SetCustomIcon("<img src='images/directions_pin.png'/>"); } getDirections(); } The layer that we’re returning here is basically a collection of pushpin objects. In the onFeedLoad function we’re iterating through all the pushpins and swapping out the default pushpin icon with our own one. Finally we place a call to the getDirections function :- function getDirections() { // Get the directional panel var directionsPanel = $('DirectionsPanel'); // define the HTML string var directionsHtml = ''; // Setup the distance var totalDistance = 0.0; // Fetch the shape cound. var count = COLLECTION_LAYER.GetShapeCount(); var MAP_ITEM_TITLE = 'Click to see point on map.'; // Enumerate the shapes for (var i = 0 ; i < count ; i++) { // Get the shape var shape = COLLECTION_LAYER.GetShapeByIndex(i); // Setup distance variable var distance; // If the first item if(i == 0) distance = 0.0; else distance = CalculateDistance(COLLECTION_LAYER.GetShapeByIndex(i-1).Latitude,COLLECTION_LAYER.GetShapeByIndex(i-1).Longitude,shape.Latitude, shape.Longitude); // Add to the total distance totalDistance = totalDistance + distance; // build the directions HTML (is persisted throughout the enumeration) directionsHtml += '<div class="direction">' + ' <div class="distance">' + distance.toFixed(2) + ' miles</div>' + ' <a title="' + MAP_ITEM_TITLE + '" href="JavaScript:zoom(' + shape.Latitude + ',' + shape.Longitude + ')">' + shape.GetTitle() + '</a>' + '</div>'; } // Add the totals directionsHtml += '<div class="DirectionsTotal direction heavy">' + ' <div class="distance heavy">' + totalDistance.toFixed(2) + ' miles</div>' + ' Total Distance' + '</div>'; // Add the header and add the distance to the header directionsHtml = '<h2>Directions (' + totalDistance.toFixed(2) + ' miles)</h2>' + directionsHtml; // Output directionsPanel.innerHTML = directionsHtml; } First off we get a reference to the directions panel (div) defined in the default.aspx page then we define a couple of variables, one that will hold the actual directions and the next to hold the total distance between each pushpin in our layer. Next we make a function call to get the total number of pushpins that we have added to the map layer and we round out the setting up of this function call by defining a title that we can display. The next thing we do here is to iterate through all of pushpins we have defined in our map layer one by one. Wihtin the loop, we get a reference to the actual pushpin itself :- var shape = COLLECTION_LAYER.GetShapeByIndex(i); then we call a function call that calculates the distance between this pushpin and the previous one :- function CalculateDistance(startLatitude, startLongitude, endLatitude, endLongitude) { var R = 3959; // miles // Get the Latitude differential var dLat = (endLatitude-startLatitude).toRad(); // Get the Longitude differential var dLon = (endLongitude-startLongitude).toRad(); ///TODO - add comments. var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(startLatitude.toRad()) * Math.cos(endLatitude.toRad()) * Math.sin(dLon/2) * Math.sin(dLon/2); var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); var d = R * c; return d; } Because Virtual Earth deals with latitudes and longitudes and we deal with miles, all this function does is convert the difference between the two points into miles. It’s worth taking note of this routine as it can be very handy in your applications. We take this value returned and add it to our total miles variable. Our next series of statements basically just builds a link for the current pushpin that we’re iterating against. If you click on a pushpin the map will zoom in to that point allowing you to see more detail. Once we iterate through each individual pushpin, we add another link to show the whole map layer. Basically this is a “zoom out” if you’ve zoomed in by clicking on an individual pushpin. Finally we load all of this into our Directions panel. We are now back in our main updatePage function :- if(album == "") disableAnchor($('PhotosFromRideLink'), true); else { if($('PhotosFromRideLink').disabled) disableAnchor($('PhotosFromRideLink'), false); } if(cid == "") { disableAnchor($('MapLink'), true); disableAnchor($('DirectionsLink'), true); disableAnchor($('ViewRouteLink'), true); disableAnchor($('BikeCamLink'), true); } else { if($('MapLink').disabled) disableAnchor($('MapLink'), false); if($('DirectionsLink').disabled) disableAnchor($('DirectionsLink'), false); if($('ViewRouteLink').disabled) disableAnchor($('ViewRouteLink'), false); if($('BikeCamLink').disabled) disableAnchor($('BikeCamLink'), false); } // Hard coded link for London Bike Cam demo switch(title.toLowerCase()) { case "london river thames": if($('BikeCamLink').disabled) disableAnchor($('BikeCamLink'), false); break; default: disableAnchor($('BikeCamLink'), true); break; } Here we are dealing with the MenuPanel and we’re simply going over which of the 6 options that are displayed in the panel. Firstly we’re checking to see if there’s a photo album associated with the blog post. If there is then activate the “Photos from Ride” menu button, otherwise disable it. Next we check whether there is an actual map collection associated with the blog post. The reason for this is that some items listed in the Events user control (which links to the Events RSS feed) may not have been defined yet and therefore will not have a map associated with them. If a map does indeed exist then enable all the buttons that pertain to the map (View Route, Map, Directions and Bike Cam) otherwise disable these buttons. One of the rides (London River Thames) has a video associated it. The next series of statements checks to see if we are displaying the details for this blog post, if so then we enable the Bike CAM button, otherwise disable it. And that’s all there is to it. The updatePage javascript function is basically the main routine for this application which sets up and displays the main portion of the default.aspx but as you’ve seen, it’s fairly straight forward. November 06 Windows Live Quick Apps – Contoso Bicycle Club Part 2Welcome back to the second part of our deep dive in the Windows Live Quick Apps Website featuring the Contoso Bicycle Club. In this part we’ll take a look at another custom user control found on the home page, the Latest Rides feed. If you followed the first part of this deep dive then you will find a lot of things very similar with this control :- <%@ Control Language="C#" ClassName="LatestRidesControl" %> <div id="latestRidesContent"> <script runat="server"> int n = 1; </script> <!-- setup the XML Datasource - this will query the RSS feed directly --> <asp:XmlDataSource ID="latestRidesDataSource" runat="server" EnableCaching="false" DataFile="<%$ AppSettings:LatestRidesFeed %>" XPath="/rss/channel/item"></asp:XmlDataSource> <table> <asp:DataList ID="latestRidesDataList" runat="server" DataSourceID="latestRidesDataSource"> <ItemTemplate> <tr class="latestRideItem"> <td valign="top" class="left"> <a href="javascript:showDiv($('contentPanel'), 100);updatePage('<%# XPath("title") %>','<%= latestRidesDataSource.DataFile %>', <%= n%> ,'<%# HtmlProcessor.ExtractMapCid(XPath("description").ToString()) %>','<%# HtmlProcessor.ExtractPhotoAlbumFeed(XPath("description").ToString()) %>')"> <img width="50" height="50" alt="<%# XPath("title") %>" src="<%# HtmlProcessor.ExtractImageUrl(XPath("description").ToString()) %>" /> </a> </td> <td class="right"> <div class="title"> <a href="javascript:showDiv($('contentPanel'), 100);updatePage('<%# XPath("title") %>','<%= latestRidesDataSource.DataFile %>', <%= n++ %> ,'<%# HtmlProcessor.ExtractMapCid(XPath("description").ToString()) %>','<%# HtmlProcessor.ExtractPhotoAlbumFeed(XPath("description").ToString()) %>')"> <%# XPath("title") %> </a> </div> <div class="description"> <%# HtmlProcessor.FirstLine(XPath("description").ToString()) %></div> </td> </tr> </ItemTemplate> </asp:DataList> </table> </div> So let’s start at the top. The first thing you’ll see is a counter has been setup and initialized to 1. This serves the same purpose as the counter found in Events Control and simply gives each entry its own unique ID. Next we set up the XML DataSource. This time however we’re point to a different entry in the web config file :- <add key="LatestRidesFeed" value="http://contosobicycleclub.spaces.live.com/category/rides/feed.rss"/> In the Event Control we pointed to the RSS feed gained from the Events category in the CBC Windows Live Spaces site, this time we’re pointing to the Rides feed. This feed will be used in more than one place in the sample web site but we’ll talk about it again when we get to that point in our deep dive. The XPath expression for our DataSource is the same as the one used the DataSource control in the Events Control. This is because the feed that is being distributed is based on a standard. You will find the same xml markup tags in every feed produced by Windows Live Spaces. This makes our jobs as developers easier. XPath="/rss/channel/item" Here we are saying, from the root of the received feed, we are only interested in the xml that is encapsulated in the Item tag. The Item tag is found within the Channel tag which itself is found in the RSS tag under the root. The easy way to think about this is a folder hierarchy as you might find on your computer. An equivalent would be C:/Windows/System32. Here we’re looking for the contents of the System32 folder which is found under the Windows folder which is found under the root of the C: drive. XPath expressions were designed this way so as to be easy to use and understand. Next we start a table. This is being used purely for layout purposes as this control is slightly more complex than the Event Control and displays more data. Within the table tag we start our repeating section using the DataList control. The DataList control gets its data from the XmlDataSource that we defined above. We define a table row and a couple of table cells. As mentioned this is purely for presentation purposes. Within the first table cell we display an image. <a href=" javascript:showDiv($('contentPanel'), 100);updatePage('<%# XPath("title") %>','<%= latestRidesDataSource.DataFile %>', <%= n%> ,'<%# HtmlProcessor.ExtractMapCid(XPath("description").ToString()) %>','<%# HtmlProcessor.ExtractPhotoAlbumFeed(XPath("description").ToString()) %>')"> <img width="50" height="50" alt="<%# XPath("title") %>" src="<%# HtmlProcessor.ExtractImageUrl(XPath("description").ToString()) %>" /> </a> Don’t worry, it looks way more complicated than it actually is. First, we define a link and rather than the link taking us to new page or section we’re running some custom Javascript inside it. The first statement simply calls a Javascript function that un-hides a DIV section that is defined in the default.aspx page. function showDiv(div, zIndex) { div.style.visibility = 'visible'; div.style.zIndex = zIndex; // Work around for Bugzilla Bug 187435 for Firefox on Mac if(div.id.toLowerCase() == "directionspanel" || div.id.toLowerCase() == "textpanel" || div.id.toLowerCase() == "mainpanel") { div.style.overflow = "auto"; } } As you can see from this javascript function, we simply take a reference to the div and show it with a z-index that is also passed in. The only thing slightly unusual about this is the call to this function :- javascript:showDiv($('contentPanel'), 100); The javascript function showDiv is expecting a reference to the actual Div, not the name of the name of the Div as you may guess from contentPanel being enclosed in quotes. This is actually using a short cut notation that is found in Asp.Net Ajax. The actual expression is “$(‘contentPanel’)”. This is equivalent of saying :- Document.getElementById(‘contentPanel’) Which would be a reference to the Div itself and not just the name of the Div. Next we have a call to another JavaScript function. This function call is slightly more complex as it is a generic function for use by virtually every control on the page and therefore there are some parameters to it that are essentially optional, depending on where the function is being called from. So lets go through this step by step. The first parameter in the updatePage function call is title of the blog post :- <%# XPath("title") %> Remember that we have already extracted the xml down the item level. Each Item is a blog post itself. So here we’re simply saying, give me the contents of what’s found in the Title Xml element in the Item. The next parameter is the whole RSS feed itself :- '<%= latestRidesDataSource.DataFile %>' Next we pass in the value of the counter that was defined at the top of the user control. This essentially gives each repetition its own ID. The next call is to static member of a class file found in the Classes folder of the website :- <%# HtmlProcessor.ExtractMapCid(XPath("description").ToString()) %> We make a call to ExtractMapCid method, this returns an id for a virtual earth map collection that is taken from within the description of the blog post. What is this? If you open up http://maps.live.com, in the top right of the page you’ll see a dropdown for Collections. This was a feature added to virtual earth V5. You can create your own collections or browse other peoples collections that they have made public. Each collection has its own unique ID. Here is the code that we are calling :- public static string ExtractMapCid(string html) { // Create the RegEx (conditions are find anything (including URLs) which have cid={THECIDVALUE} Regex regex = new Regex(";cid=(.*?)&"); // Execute a search/match against the regex Match match = regex.Match(html); if (match.Groups.Count > 1) return match.Groups[1].Value; else return ""; } What we’re doing here is parsing through the contents of the Description XML tag looking for anything that matches “;cid=” and the cid value. This cid value is the unique ID given to collections within the maps.live.com application and extracting that value out to be passed into our updatePage javascript function. The final parameter for out updatePage function call makes a call to another static method found in the HtmlProcessor class file :- <%# HtmlProcessor.ExtractPhotoAlbumFeed(XPath("description").ToString()) %> This time (as the function name explains) we are looking for photoalbums. public static string ExtractPhotoAlbumFeed(string html) { // Create a regex to find a spaces photoalbum url. Regex regex = new Regex("http://(.*?).spaces.live.com/.*?PhotoAlbum.*?(cns!.*?)&"); Match match = regex.Match(html); if (match.Groups.Count > 2) return string.Format("http://{0}.spaces.live.com/photos/{1}/feed.rss", match.Groups[1].Value, match.Groups[2].Value); else return ""; } This is a snippet from one of the blog posts :- Finished ride near some really good places to eat at London Bridge.<br></span> </div> <div> <p><a href="http://maps.live.com/default.aspx?v=2&cid=2BACE20A0AB578FB!180&encType=1">Map</a> <p><a href="http://contosobicycleclub.spaces.live.com/?_c11_PhotoAlbum_spaHandler=TWljcm9zb2Z0LlNwYWNlcy5XZWIuUGFydHMuUGhvdG9BbGJ1bS5GdWxsTW9kZUNvbnRyb2xsZXI$&_c11_PhotoAlbum_spaFolderID=cns!19C180FDFB1C7EFF!138&_c11_PhotoAlbum_startingImageIndex=0&_c11_PhotoAlbum_commentsExpand=0&_c11_PhotoAlbum_addCommentExpand=0&_c11_PhotoAlbum_addCommentFocus=0&_c=PhotoAlbum">Photos</a></div><div> As you can see it’s basically HTML markup. Within here you will notice there is a link tag pointing to a photo album that matches our regular expression :- The regular expression splits this down into 3 separate groups. The first group is the whole string above, the second is initial value found after http:// as defined by :- And the third group is the cns number as defined in the regular expression by :- (cns!.*?) What we are really looking for is the second and third values. These are plugged into the return string :- string.Format("http://{0}.spaces.live.com/photos/{1}/feed.rss",match.Groups[1].Value, match.Groups[2].Value); And so what you actually end up with is this url :- http://contosobicycleclub.spaces.live.com/photos/cns!19C180FDFB1C7EFF!138/feed.rss In the middle of the link (anchor tag) an image is placed :- <img width="50" height="50" alt="<%# XPath("title") %>" src="<%# HtmlProcessor.ExtractImageUrl(XPath("description").ToString()) %>" /> Following the same logic we have thus far, the alt parameter is defined as the title of blog and src parameter point to what is returned from the call to ExtractImageUrl, another static function call found in the HtmlProcessor class :- public static string ExtractImageUrl(string html) { // Create a RegEx to find the Image Regex regex = new Regex("<img.*?src.*?=.*?\"(.*?)\"", RegexOptions.IgnoreCase); // Execute the search/match. Match match = regex.Match(html); if (match.Groups.Count > 1) return match.Groups[1].Value; else return ""; } Once again we are using regular expression pattern matching to look for a specific value. If you take a look at the source for the RSS feed, look towards the end of the first <item> tag, just before all the comments and you’ll see the following (even if you just look at the rss feed itself in IE7, you’ll see a picture attached to the end of each post) :- img src="http://storage.live.com&#47;items&#47;19C180FDFB1C7EFF&#33;131&#58;thumbnail This is what we’re extracting out, the value for the source of this image tag. The contents of the second table cell are exactly the same as the content for the Event Control (see previous article for details). The only difference between the two is that the second and third parameters for the updatePage function call actually have values this time (as explained above for the first table cell). And there you have it, the Latest Rides user control from the Contoso web site. It’s almost identical to the Events user control, but this time we looked at a different feed and extracted out slightly more information from the description tag to enable us to display images (and also information that will be used by other controls on the page but isn’t really apparent in the display of the Latest Rides control). So let’s extract what we’ve learned here and build upon our very basic page from the previous article. Open up the website you created from the previous article and open the default.aspx page. What we’ll be doing is simply adding an image next to the title for each blog post and display the first image we find that is contained within the blog in this spot. So here is the default page from yesterday :- <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:TextBox ID="Feed" runat="server" Columns="60" /> <br /> <asp:Button ID="btnSubmit" runat="server" OnClick="btnSubmit_Click" Text="Get Feed" /> <br /> <br /> <asp:XmlDataSource ID="source" runat="server" XPath="/rss/channel/item" DataFile="http://msnwindowslive.spaces.live.com/feed.rss" /> <asp:Repeater DataSourceID="source" ID="DisplayFeed" runat="server"> <ItemTemplate> <div> <a href='<%# XPath("link") %>'> <%# XPath("title") %> </a> <hr /> <%# GetFirstXCharacters(XPath("description").ToString(), 900)%> <br /> <br /> </div> </ItemTemplate> </asp:Repeater> </div> </form> </body> </html> Now all that we need to do is insert the following image tag just before the <%# XPath("title") %> that is found in the link :- <img width="50" height="50" alt="<%# XPath("title") %>" src="<%# ExtractPics(XPath("description").ToString()) %>" /> Now go into your code behind and add the ExtractPics static method as so :- public static string ExtractPics(string description) { Regex regex = new Regex("<img.*?src.*?=.*?\"(.*?)\"", RegexOptions.IgnoreCase); Match match = regex.Match(description); if (match.Groups.Count > 1) return match.Groups[1].Value; else return ""; } This is basically the same pattern matching expression that we discussed above in the article. And that’s all there is to it. In the next part of this deep dive we’ll be taking a look at the main body of the page which is really where the fun starts. August 08 Windows Live ID - Part 3 - Putting it all togetherIn the previous two articles I showed you how to use the Windows Live ID login control in your web site then we developed a custom Membership provider so that you could hook Windows Live ID in with asp.net's build in user management features. This article will show you it's easy to hook these things together. A lot of the code you in this article should be familiar to you by now, it really is just a case of hooking them together. The first thing you'll need to do is create the database structure, stored procedures etc. that the provider model expects. This is easily accomplished by running the aspnet_regsql wizard which is found in c:\windows\microsoft.net\framework\v2.0.50727. We will use our existing web site that we created in the first article. So open that up and then open up your web.config file. There are a couple of things we need to add to this, namely the connection string to your database and the membership provider (you should already have the appSettings that contains your secret and appID from the first article. Since we also used the Microsoft Enterprise library for database access in the membership provider, we need to add that as well. So in your web.config file you should have a configSections that looks like the following :- <configSections> <section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication" /> <sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"> <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="Everywhere" /> <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication" /> <section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication" /> </sectionGroup> </sectionGroup> </sectionGroup> </configSections> Most of this is for Microsoft ASP.Net Ajax support but notice the top entry, this is the entry for the enterprise library. <dataConfiguration defaultDatabase="Connection String" /> This simply points to your database connection string which should be coded as normal :- <connectionStrings> <add name="Connection String" connectionString="Data Source=YourServer;Initial Catalog=LiveID;Persist Security Info=True;User ID=userid;Password=pwd" providerName="System.Data.SqlClient" /> </connectionStrings> Just incase you haven't already got it in your web.config file from the first part of this article, add an appsettings section and in there put in your secret and appID (see first article for more details) :- <appSettings > <add key="secret" value="YourSecret"/> <add key="appid" value="000000000000000"/> </appSettings> <system.web> Finally we need to add a reference to our membership provider and also tell our web site that we will be using forms based authentication :- <membership defaultProvider="LiveIDMembershipProvider"> <providers> <add description="Custom Membership provider connecting to central datastore" connectionStringName="Connection String" name="LiveIDMembershipProvider" type="Hackersoft.UserRepository.LiveIDMembershipProvider"/> </providers> </membership> <authentication mode="Forms"/> Here we're just saying use our custom membership provider. Our membership provider will be using the same database connection string as the Enterprise library (infact it does use the enterprise library) and we set our web site to forms authentication. And that's all the changes we need to make to our web.config file. In your website, add the LiveIDMembershipProvider from the second part of this series into the App_Code directory. Also make sure you have added the appropriate enterise library dll's and the WindowsLiveAuthLite.dll into your bin directory (see previous articles for more details on this). So now we are ready for our site. This will be a short demonstration containing two pages, the default page contains the Windows Live ID login control and a label that we'll use to display messages. If you have already registered with the site, then after you sign in using the Windows Live ID control, the label on this page will display a welcome message, if you have not registered then you will be redirected to another page where we will collect some information from you. In this demo we will collect the least amount of information necessary, an email address. First off our default page :- <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> This page simply contains the Windows Live ID login component contained within the iFrame and our Label control. using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using WindowsLiveAuthLite; using System.Web.Profile; using Hackersoft.UserRepository;
public partial class _Default : System.Web.UI.Page { protected string AppID; protected string context; private string userid; private LiveIDMembershipProvider mem = new LiveIDMembershipProvider();
protected void Page_Load(object sender, EventArgs e) { WindowsLiveLogin wll = new WindowsLiveLogin(); AppID = wll.AppId; context = "Register.aspx";
HttpRequest req = HttpContext.Current.Request; string action = req.QueryString.Get("action"); if (action == "clearcookie") { byte[] content; string type; wll.GetClearCookieResponse(out type, out content); HttpResponse res = HttpContext.Current.Response; res.ContentType = type; res.OutputStream.Write(content, 0, content.Length); res.End(); } else { WindowsLiveLogin.User user = wll.ProcessLogin(req.Form); if (user != null) { userid = user.Id; //txt.Text = "Your ID = " + user.Id; //txt.Text += "<br> IsSane = " + user.IsSane(); //txt.Text += "<br> Token = " + user.Token.ToString(); //txt.Text += "<br> Context = " + user.Context.ToString(); if (mem.ValidateUser(user.Id, user.Id, "LiveIDExample")) { MembershipUser memuser = mem.GetUser(user.Id, "LiveIDExample", false); Welcome.Text = "Welcome back " + memuser.Email; } else { Server.Transfer(user.Context.ToString() + "?ID=" + user.Id); } } } }
} We add a couple of references in our "using" section, pointing to the namespaces for our custom membership provider and WindowsLiveAuthLite. if (mem.ValidateUser(user.Id, user.Id, "LiveIDExample")) { MembershipUser memuser = mem.GetUser(user.Id, "LiveIDExample", false); Welcome.Text = "Welcome back " + memuser.Email; } else { Server.Transfer(user.Context.ToString() + "?ID=" + user.Id); } Here we try to validate the user using our custom membership provider. If the user validates then we display the welcome message. If the user does not validate then that means the user hasn't registered with our site so we pass them through to our registration page. If you looked at the top of the Page Load event, we passed in a context of "Registration.aspx" to Windows Live ID. Here we simply transfer to this page passing through the unique user ID returned by the Windows Live ID control. So our page now looks like this with simply the Windows Live ID control displayed when you first visit it :- Our second page is our registration page. On this page I am simply asking the user for their Email address so I have a label, textbox and a submit button. You could of course ask the user for more information depending on what it is that you wish to capture for your sites use. :- <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Register.aspx.cs" Inherits="Register" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > </form> Nothing fancy there. And the corresponding code behind file :- using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using Hackersoft.UserRepository; using WindowsLiveAuthLite;
public partial class Register : System.Web.UI.Page { protected string AppID; protected string context;
protected void Page_Load(object sender, EventArgs e) {
}
protected void btnSubmit_Click(object sender, EventArgs e) { int status = 0; LiveIDMembershipProvider mem = new LiveIDMembershipProvider(); MembershipUser user = null; user = mem.CreateUser(Request["ID"].ToString(), Request["ID"].ToString(), "LiveIDExample", txtEmail.Text, string.Empty, string.Empty, out status); //Response.Redirect("Default.aspx"); } } Again nothing fancy. All we are dealing with is the button click event. In the button click event we create a new instance of our membership provider then simply use the CreateUser method to create a new user in our databases. The CreateUser method was explained in more detail in the second part of this series. We're simply passing in the ID returned to us by the Windows Live ID control as the username and password, "LiveIDExample" is the Application Name. And that's basically it. If you go back to the Default page, we have a check in place to see if the user is a member and if they are we display a welcome message :- The user is stored in the Asp.Net database and you can access it and use it just like you would normally, assign roles, profile information etc. etc. Technorati Tags: Asp.Net Windows Live Windows Live ID Programming July 06 Windows Live ID - Part 2 - The Code Part 2The Code part 2 :- public bool UnlockUser(string username, string applicationname) { int ReturnValue = 0; Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_UnlockUser"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@Username", username); cmdSQL.CommandType = CommandType.StoredProcedure; ReturnValue = Convert.ToInt32(db.ExecuteScalar(cmdSQL)); if (ReturnValue == 0) { return true; } else { return false; } }
public bool DeleteUser(string username, string applicationname, bool deleteallrelateddata) { int UserDeleted = 0; int ErrorCode = 0; Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Users_DeleteUser"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@UserName", username); if (deleteallrelateddata == true) { cmdSQL.Parameters.AddWithValue("@TablesToDeleteFrom", 15); } else { cmdSQL.Parameters.AddWithValue("@TablesToDeleteFrom", 0); } SqlParameter output = new SqlParameter("@NumTablesDeletedFrom", SqlDbType.Int); output.Direction = ParameterDirection.Output; //SqlParameter errorcode = new SqlParameter("@ErrorCode", SqlDbType.Int); //errorcode.Direction = ParameterDirection.ReturnValue; cmdSQL.Parameters.Add(output); //cmdSQL.Parameters.Add(errorcode); cmdSQL.CommandType = CommandType.StoredProcedure; db.ExecuteNonQuery(cmdSQL); UserDeleted = Convert.ToInt32(cmdSQL.Parameters["@NumTablesDeletedFrom"].Value.ToString()); //ErrorCode = Convert.ToInt32(cmdSQL.Parameters["ErrorCode"].Value.ToString()); if (UserDeleted == 0) { return false; } else { return true; } }
public bool ChangePassword(string username, string oldpassword, string newpassword, string applicationname) { int ReturnCode = 0; string PasswordSalt = CreateSalt(5); string HashedPassword = CreatePasswordHash(newpassword, PasswordSalt); bool OldPasswordValidated = ValidateUser(username, oldpassword, applicationname); if(OldPasswordValidated == false) { return false; } Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_SetPassword"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@UserName", username); cmdSQL.Parameters.AddWithValue("@NewPassword", HashedPassword); cmdSQL.Parameters.AddWithValue("@PasswordSalt", PasswordSalt); cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", DateTime.Now); cmdSQL.Parameters.AddWithValue("@PasswordFormat", MembershipPasswordFormat.Clear); SqlParameter returncode = new SqlParameter("@ReturnCode", SqlDbType.Int); returncode.Direction = ParameterDirection.ReturnValue; cmdSQL.CommandType = CommandType.StoredProcedure; db.ExecuteNonQuery(cmdSQL); ReturnCode = Convert.ToInt32(cmdSQL.Parameters["@ReturnCode"].Value.ToString()); if (ReturnCode == 0) { return true; } else { return false; } }
public void UpdateUser(string applicationname, MembershipUser user) { Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_UpdateUser"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@UserName", user.UserName); cmdSQL.Parameters.AddWithValue("@Email", user.Email); cmdSQL.Parameters.AddWithValue("@Comment", user.Comment); cmdSQL.Parameters.AddWithValue("@IsApproved", user.IsApproved); cmdSQL.Parameters.AddWithValue("@LastLoginDate", user.LastLoginDate); cmdSQL.Parameters.AddWithValue("@LastActivityDate", user.LastActivityDate); cmdSQL.Parameters.AddWithValue("@UniqueEmail", 1); //Requires Unique Email cmdSQL.Parameters.AddWithValue("@CurrnetTimeUtc", DateTime.Now); SqlParameter returncode = new SqlParameter("@ReturnCode", SqlDbType.Int); returncode.Direction = ParameterDirection.ReturnValue; cmdSQL.CommandType = CommandType.StoredProcedure; db.ExecuteNonQuery(cmdSQL); //Since we are returning void, do we really need to check the //return type? Only course of action would be to throw a //exception which won't work as this is a web service. }
public int GetNumberOfUsersOnline(string applicationname) { Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_GetNumberOfUsersOnline"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@MinutesSinceLastInActive", 20); //value of IIS timeout cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", DateTime.Now); SqlParameter returnvalue = new SqlParameter("@NumOnline", SqlDbType.Int); returnvalue.Direction = ParameterDirection.ReturnValue; cmdSQL.Parameters.Add(returnvalue); db.ExecuteNonQuery(cmdSQL); return Convert.ToInt32(cmdSQL.Parameters["@NumOnline"].Value.ToString()); }
public MembershipUserCollection FindUsersByName(string applicationname, string username) { DataSet ds = null; int RecordstoGo = 1; int TotalRecords = 0; int PageIndex = 0; MembershipUserCollection users = new MembershipUserCollection(); Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_FindUsersByName"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@UserNameToMatch", username); cmdSQL.Parameters.AddWithValue("@PageIndex", PageIndex); cmdSQL.Parameters.AddWithValue("@PageSize", 1000); //Get 1000 users at a time SqlParameter NumberofUsers = new SqlParameter("@TotalRecords", SqlDbType.Int); NumberofUsers.Direction = ParameterDirection.ReturnValue; cmdSQL.Parameters.Add(NumberofUsers); ds = db.ExecuteDataSet(cmdSQL); TotalRecords = Convert.ToInt32(cmdSQL.Parameters["TotalRecords"].Value.ToString()); RecordstoGo = TotalRecords; do { if (ds.Tables[0].Rows.Count > 0) { for (int i = 0; i < ds.Tables[0].Rows.Count; i++) { DataRow dr = ds.Tables[0].Rows[i]; MembershipUser user = PopulateMembershipUser(dr); users.Add(user); } } RecordstoGo -= 1000; if (RecordstoGo > 0) { PageIndex += 1; cmdSQL.Parameters["@PageIndex"].Value = PageIndex; ds = db.ExecuteDataSet(cmdSQL); } } while (RecordstoGo > 0); return users;
}
public MembershipUserCollection GetAllUsers(string applicationname) { DataSet ds = null; int RecordstoGo = 1; int TotalRecords = 0; int PageIndex = 0; MembershipUserCollection users = new MembershipUserCollection(); Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_GetAllUsers"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@PageIndex", PageIndex); cmdSQL.Parameters.AddWithValue("@PageSize", 1000); //Get 1000 users at a time SqlParameter NumberofUsers = new SqlParameter("@TotalRecords", SqlDbType.Int); NumberofUsers.Direction = ParameterDirection.ReturnValue; cmdSQL.Parameters.Add(NumberofUsers); ds = db.ExecuteDataSet(cmdSQL); TotalRecords = Convert.ToInt32(cmdSQL.Parameters["TotalRecords"].Value.ToString()); RecordstoGo = TotalRecords; do { if (ds.Tables[0].Rows.Count > 0) { for (int i = 0; i < ds.Tables[0].Rows.Count; i++) { DataRow dr = ds.Tables[0].Rows[i]; MembershipUser user = PopulateMembershipUser(dr); users.Add(user); } } RecordstoGo -= 1000; if (RecordstoGo > 0) { PageIndex += 1; cmdSQL.Parameters["@PageIndex"].Value = PageIndex; ds = db.ExecuteDataSet(cmdSQL); } } while (RecordstoGo > 0); return users; }
public MembershipUserCollection FindUsersByEmail(string applicationname, string email) { DataSet ds = null; int RecordstoGo = 1; int TotalRecords = 0; int PageIndex = 0; MembershipUserCollection users = new MembershipUserCollection(); Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_FindUsersByEmail"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@EmailToMatch", email); cmdSQL.Parameters.AddWithValue("@PageIndex", PageIndex); cmdSQL.Parameters.AddWithValue("@PageSize", 1000); //Get 1000 users at a time SqlParameter NumberofUsers = new SqlParameter("@TotalRecords", SqlDbType.Int); NumberofUsers.Direction = ParameterDirection.ReturnValue; cmdSQL.Parameters.Add(NumberofUsers); ds = db.ExecuteDataSet(cmdSQL); TotalRecords = Convert.ToInt32(cmdSQL.Parameters["TotalRecords"].Value.ToString()); RecordstoGo = TotalRecords; do { if (ds.Tables[0].Rows.Count > 0) { for (int i = 0; i < ds.Tables[0].Rows.Count; i++) { DataRow dr = ds.Tables[0].Rows[i]; MembershipUser user = PopulateMembershipUser(dr); users.Add(user); } } RecordstoGo -= 1000; if (RecordstoGo > 0) { PageIndex += 1; cmdSQL.Parameters["@PageIndex"].Value = PageIndex; ds = db.ExecuteDataSet(cmdSQL); } } while (RecordstoGo > 0); return users; }
#region Overriden methods not used public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { throw new NotSupportedException(); }
public override MembershipUser GetUser(string username, bool userIsOnline) { throw new NotSupportedException(); }
public override string GetUserNameByEmail(string email) { throw new NotSupportedException(); }
public override bool ValidateUser(string username, string password) { throw new NotSupportedException(); }
public override bool UnlockUser(string userName) { throw new NotSupportedException(); }
public override bool DeleteUser(string username, bool deleteAllRelatedData) { throw new NotSupportedException(); }
public override bool ChangePassword(string username, string oldPassword, string newPassword) { throw new NotSupportedException(); }
public override void UpdateUser(MembershipUser user) { throw new NotSupportedException(); }
public override int GetNumberOfUsersOnline() { throw new NotSupportedException(); }
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); }
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); }
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { throw new NotSupportedException(); }
public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { throw new NotSupportedException(); }
public override string GetPassword(string username, string answer) { throw new NotSupportedException(); }
public override string ResetPassword(string username, string answer) { throw new NotSupportedException(); }
#endregion #region Helper Methods private string CreatePasswordHash(string password, string salt) { string SaltAndPassword = string.Concat(password, salt); //Now hash the salted password string HashedPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(SaltAndPassword, "SHA1"); return HashedPassword; }
private string CreateSalt(int size) { //Generate a cryptographic random number using the cryptographic service provider System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider(); byte[] buffer = new byte[size]; rng.GetBytes(buffer); return Convert.ToBase64String(buffer); }
private string GetApplicationID(string applicationname) { Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Personalization_GetApplicationId"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); SqlParameter output = new SqlParameter("@ApplicationID", SqlDbType.UniqueIdentifier); output.Direction = ParameterDirection.Output; output.Value = null; cmdSQL.Parameters.Add(output); cmdSQL.CommandType = CommandType.StoredProcedure; db.ExecuteNonQuery(cmdSQL); return cmdSQL.Parameters["@ApplicationID"].Value.ToString(); }
private string CreateApplication(string applicationname) { Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Applications_CreateApplication"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); SqlParameter output = new SqlParameter("@ApplicationID", SqlDbType.UniqueIdentifier); output.Value = null; output.Direction = ParameterDirection.Output; cmdSQL.Parameters.Add(output); db.ExecuteNonQuery(cmdSQL); return cmdSQL.Parameters["@ApplicationID"].Value.ToString(); }
private MembershipUser PopulateMembershipUser(DataRow dr) { MembershipUser user = new MembershipUser(m_ProviderName, dr["UserName"].ToString(), new Guid(dr["UserID"].ToString()), dr["Email"].ToString(), dr["PasswordQuestion"].ToString(), dr["Comment"].ToString(), Convert.ToBoolean(dr["IsApproved"]), Convert.ToBoolean(dr["IsLockedOut"]), Convert.ToDateTime(dr["CreateDate"]), Convert.ToDateTime(dr["LastLoginDate"]), Convert.ToDateTime(dr["LastActivityDate"]), Convert.ToDateTime(dr["LastPasswordChangedDate"]), Convert.ToDateTime(dr["LastLockoutDate"])); return user; } #endregion } }
And that's all there is to it. Creating your own membership providers really isn't that hard and now we have something that we can use in conjunction with Live ID authentication.
In the last part of this series of articles I'll explain how to put all of this together. Windows Live ID - Part 2In the first part of this article I explained how to integrate Windows Live ID into your own web site, how to call it and what information it returned and how to access that information. Now that is all fine and well but if you're using ASP.Net 2.0 (and above) and you want to take advantage of the inbuilt memberships, roles etc. the information returned from Live ID doesn't make this easy on you. So how do you do this? Well there are a number of different ways to do this but possibly the easiest is to create your own MemberShip provider and use to store and access the information. In this article we will be building a Membership provider that you can call with the very basic information that Live ID returns to you (basically the only useful thing it returns is a static ID string that will always be consistent for your registered web site. Extending or building your own MemberShip provider isn't that hard, you simply create a new class that inherits from MembershipProvider and override the methods with your own code. Unfortunately there is a fair amount of code to this but most of it is fairly intuitive. A couple of Notes before we proceed :-
So lets start. Create a new Class library file in your App_Code directory and name it LiveIDMembershipProvider.cs. using System; using System.Data; using System.Data.SqlClient; using System.Configuration; using System.Web; using System.Web.Security; using Microsoft.Practices.EnterpriseLibrary.Data; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration.Provider; using System.Web.Hosting; using System.Web.Management; using System.Security.Permissions; Note the reference to Microsoft.Practices.EnterpriseLibrary.Data which was explained above. Next create your class :- namespace Hackersoft.UserRepository { public class LiveIDMembershipProvider : MembershipProvider { You can give your class any namespace you wish and in the class declaration we are stating that we want to inherit from MembershipProvider. Create some class member variables :- static string m_ProviderName = "LiveIDMembershipProvider"; static int m_InvalidPasswordAttempts = 1; static int m_NonAlphaCharacters = 0; static int m_PasswordLength = 3; static int m_PasswordAttemptWindow = 5; private string m_ApplicationName = string.Empty;
The m_ProviderName variable you can set to whatever you like. The other variables are fairly self explanatory, the number of password attempts you will allow doesn't really count as we will be using Live ID for authentication, same with PasswordLength which states the minimum length of the password. We will be using a hashed version of the user ID returned by Live ID as the password.
Next we add some public properties to set or read (mostly read) the member variables above. The only one which is read/write is the Application Name. The way that the .Net database is setup is that you can have multiple sites running off a single database and authentication against a specific site is done by passing through the name of the application that you wish to authenticate against. public override string ApplicationName { get { return m_ApplicationName; } set { m_ApplicationName = value; } }
public override bool EnablePasswordRetrieval { get { return false; } }
public override bool EnablePasswordReset { get { return false; } }
public override int MaxInvalidPasswordAttempts { get { return m_InvalidPasswordAttempts; } }
public override int MinRequiredNonAlphanumericCharacters { get { return m_NonAlphaCharacters; } }
public override int MinRequiredPasswordLength { get { return m_PasswordLength; } }
public override int PasswordAttemptWindow { get { return m_PasswordAttemptWindow; } }
public override MembershipPasswordFormat PasswordFormat { get { return MembershipPasswordFormat.Clear; } }
public override string PasswordStrengthRegularExpression { get { throw new NotSupportedException(); } }
public override bool RequiresQuestionAndAnswer { get { return false; } }
public override bool RequiresUniqueEmail { get { return true; } } The first method in your custom membership provider that you will want to override is the CreateUser method :- public MembershipUser CreateUser(string username, string password, string applicationname, string email, string passwordquestion, string passwordanswer, out int status) { //This calls the Membership_CreateUser SPROC. //The CreateUser SPROC will automatically create an application if one doesn't exist //Create the User account and also create the Membership account. //The output to this is the newly created UserID. string UserID = string.Empty; string PasswordSalt = CreateSalt(5); string PasswordHash = CreatePasswordHash(password, PasswordSalt); status = 0; Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_CreateUser"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@UserName", username); cmdSQL.Parameters.AddWithValue("@Password", PasswordHash); cmdSQL.Parameters.AddWithValue("@PassWordSalt", PasswordSalt); cmdSQL.Parameters.AddWithValue("@Email", email); cmdSQL.Parameters.AddWithValue("@PasswordQuestion", passwordquestion); cmdSQL.Parameters.AddWithValue("@PasswordAnswer", passwordanswer); cmdSQL.Parameters.AddWithValue("@IsApproved", 1); cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", DateTime.Now); cmdSQL.Parameters.AddWithValue("@CreateDate", DateTime.Now); cmdSQL.Parameters.AddWithValue("@UniqueEmail", 1); cmdSQL.Parameters.AddWithValue("@PasswordFormat", MembershipPasswordFormat.Clear); SqlParameter output = new SqlParameter("@UserID", SqlDbType.UniqueIdentifier); output.Direction = ParameterDirection.Output; cmdSQL.Parameters.Add(output); cmdSQL.CommandType = CommandType.StoredProcedure; try {
status = Convert.ToInt32(db.ExecuteNonQuery(cmdSQL)); UserID = cmdSQL.Parameters["@UserID"].Value.ToString();
} catch (Exception ex) { //put your exception handling here } MembershipUser user = GetUser(new Guid(UserID), true); //status = Error; return user; } This method returns a Membership user object (which is ultimately what you want). We call the standard SQL stored procedure provided for us and are simply poking our own custom values into this. The four main parameters that you will need to provide are username, password, application name and email. Both username and password will be the user ID returned from the Live ID service, Application Name will be the name of your application (see above for short explanation) and email is the minimum data that you must collect from the user on your registration page. Password question and password answer you can simply set to string.Empty. There are two overrides of the GetUser method, the one that we called in the CreateUser method above takes the UserID (as a GUID) and a flag indicating whether the user is online or not and returns a MembershipUser object. public override MembershipUser GetUser(object provideruserkey, bool userisonline) { MembershipUser user = null; Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_GetUserByUserId"); cmdSQL.Parameters.AddWithValue("@UserID", provideruserkey); cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", DateTime.Now); cmdSQL.Parameters.AddWithValue("@UpdateLastActivity", userisonline); cmdSQL.CommandType = CommandType.StoredProcedure; DataSet ds = db.ExecuteDataSet(cmdSQL); if(ds.Tables[0].Rows.Count > 0) { DataRow dr = ds.Tables[0].Rows[0]; user = new MembershipUser(m_ProviderName, dr["UserName"].ToString(), provideruserkey, dr["Email"].ToString(), dr["PasswordQuestion"].ToString(), dr["Comment"].ToString(), Convert.ToBoolean(dr["IsApproved"]), Convert.ToBoolean(dr["IsLockedOut"]), Convert.ToDateTime(dr["CreateDate"]), Convert.ToDateTime(dr["LastLoginDate"]), Convert.ToDateTime(dr["LastActivityDate"]), Convert.ToDateTime(dr["LastPasswordChangedDate"]), Convert.ToDateTime(dr["LastLockoutDate"])); } return user; }
As you can see, this is fairly much the same story as the CreateUser method described above. We call the Enterprise to create a connection to the database, create a SqlCommand object and tell it that we wish to call the "aspnet_Membership_GetUserByUserId" stored procedure, fill in the parameters that it expects then call the stored procedure. The real change from the CreateUser method described above is that once the database call has returned, we need to actually instantiate a MembershipUser object. The MembershipUser object expects a lot of parameters however all the information required is returned to us in the dataset from the database call. We simply plug these values in and have a now have a valid MembershipUser object.
The second override of the GetUser method is very much the same but instead of passing in a User ID we pass in the username, name of the application and again whether the user is online or not.
public MembershipUser GetUser(string username, string applicationname, bool userisonline) { MembershipUser user = null; Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_GetUserByName"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@Username", username); cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", DateTime.Now); cmdSQL.Parameters.AddWithValue("@UpdateLastActivity", userisonline); cmdSQL.CommandType = CommandType.StoredProcedure; DataSet ds = db.ExecuteDataSet(cmdSQL); if (ds.Tables[0].Rows.Count > 0) { DataRow dr = ds.Tables[0].Rows[0]; user = new MembershipUser(m_ProviderName, username, new Guid(dr["UserID"].ToString()), dr["Email"].ToString(), dr["PasswordQuestion"].ToString(), dr["Comment"].ToString(), Convert.ToBoolean(dr["IsApproved"]), Convert.ToBoolean(dr["IsLockedOut"]), Convert.ToDateTime(dr["CreateDate"]), Convert.ToDateTime(dr["LastLoginDate"]), Convert.ToDateTime(dr["LastActivityDate"]), Convert.ToDateTime(dr["LastPasswordChangeDate"]), Convert.ToDateTime(dr["LastLockoutDate"])); } return user; } The final method that I will describe is the ValidateUser method. Again it's not too difficult to follow :- public bool ValidateUser(string username, string password, string applicationname) { try { //First thing you need to do is validate the application name and get the //application ID from the database. string PasswordHash = string.Empty; string PasswordSalt = string.Empty; string HashedPassword = string.Empty; string ApplicationID = GetApplicationID(applicationname); if (ApplicationID == null || ApplicationID == string.Empty) { //Cannot find the application name therefore the user cannot //Exist for this application return false; } //Next you need to retrieve the salt and hash from the database in order to //verify the password. Database db = DatabaseFactory.CreateDatabase(); SqlCommand cmdSQL = new SqlCommand("aspnet_Membership_GetPasswordWithFormat"); cmdSQL.Parameters.AddWithValue("@ApplicationName", applicationname); cmdSQL.Parameters.AddWithValue("@Username", username); cmdSQL.Parameters.AddWithValue("@UpdateLastLoginActivityDate", 0); cmdSQL.Parameters.AddWithValue("@CurrentTimeUtc", System.DateTime.Now); DataSet ds = null; cmdSQL.CommandType = CommandType.StoredProcedure; ds = db.ExecuteDataSet(cmdSQL); if (ds.Tables[0].Rows.Count > 0) { DataRow dr = ds.Tables[0].Rows[0]; //The database does not name it's returned rows. //The Salt is stored in the first field returned (Password field) //The Hash is stored in the 3rd field returned (PasswordSalt) PasswordHash = dr[0].ToString(); PasswordSalt = dr[2].ToString(); } else { //No rows were returned from the database therefore //User does not exist. return false; } //Now we hash the password supplied. HashedPassword = CreatePasswordHash(password, PasswordSalt); //Now simply compare the hashed password just generated with that stored //in the database. If they match then username and password are correct //otherwise they are not. if (HashedPassword == PasswordHash) { return true; } else { return false; } } catch (Exception ex) { // TODO: Exception Handling routine return false; } } As you can see the ValidateUser method takes in the user name, password and the application name as parameters. We make a call to the GetApplicationID method which returns the internal application GUID. The reason we do this is to make sure that the application is actually registered in the database before we make the call to validate the user. This is basically a safety check. Again we call the Enterprise Library to create a connection to our Sql Database, create a SqlCommand object and tell it we wish to call the "aspnet_Membership_GetPasswordWithFormat" stored procedure, fill out the expected parameters and make the call. What we get in return is the password hash and salt we originally stored in the CreateUser method. Next we call the CreatePasswordHash algorithm again and pass through the password entered along with the Password Salt returned from the database call. If the returned password hash is the same as the password hash returned by the database call then the password is correct (which it really should be as the password and username are the same user ID returned by Live ID). |
|
|