<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-32442424</id><updated>2012-01-02T11:42:04.659Z</updated><category term='TIFF'/><category term='Vista'/><category term='widgetbox'/><category term='Image'/><category term='Visual Studio 2005'/><category term='Freegle'/><category term='tag'/><category term='msitran'/><category term='gadget'/><category term='Windows'/><category term='settings'/><category term='open source'/><category term='applet'/><category term='MSI'/><category term='Sort'/><category term='live spaces'/><category term='Ajax'/><category term='Forms authentication'/><category term='Multiboot'/><category term='ASP.NET'/><category term='live.com'/><category term='module'/><category term='asp:HyperLink'/><category term='Visual Studio 2008'/><category term='accessibility'/><category term='Web Setup'/><category term='orca'/><category term='liquid'/><category term='tag-aware'/><category term='SQL server'/><category term='virtual tour'/><category term='DNN'/><category term='Apache'/><category term='mashup'/><category term='JSON'/><category term='vtiger'/><category term='IE7'/><category term='text size'/><category term='Merge'/><category term='boot'/><category term='XMP'/><category term='multi-boot'/><category term='CSS'/><category term='cookies'/><category term='JPEG'/><category term='EasyBCD'/><category term='Find-in-files'/><category term='tabindex'/><category term='font'/><category term='Java'/><category term='cookieless'/><category term='IIS'/><category term='Google'/><category term='.net string UTF8'/><category term='VS2008'/><category term='Drupal'/><category term='Tomcat'/><category term='photo'/><category term='integration'/><category term='custom'/><category term='Array'/><category term='.NET ASP.NET'/><category term='DotNetNuke'/><category term='skin'/><category term='netvibes'/><category term='VPS'/><category term='CMS'/><category term='JavaScript Object Notation'/><category term='custom module'/><category term='IE'/><category term='BCD'/><category term='Internet Explorer'/><category term='Ubuntu'/><category term='command line'/><category term='ViewState'/><category term='Custom Action'/><category term='maps'/><category term='JavaScript'/><category term='widget'/><category term='.NET'/><title type='text'>Chris Cant's developer blog</title><subtitle type='html'>Here are some programming tips, be it ASP.NET, C#, CSS, Java, JavaScript, PHP, SQL, XHTML, etc.   I am director of PHD Computer Consultants Ltd, based in Cumbria, England, UK - we sell our own software and undertake software projects and consultancy.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>54</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-32442424.post-1126784059521484273</id><published>2011-08-04T09:22:00.007+01:00</published><updated>2011-08-04T10:03:25.966+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='JavaScript'/><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>Using PHDCC CodeModule with javascript</title><content type='html'>phdcc Director &lt;i&gt;John Cant&lt;/i&gt; writes:&lt;br /&gt;&lt;br /&gt;My use of DNN has become - &lt;br /&gt;&lt;br /&gt;[a] Maintain any HTML in a text file / copy and paste it into the basic editor / convert to raw / save. (Online editors are too hard to use and keep slim)&lt;br /&gt;&lt;br /&gt;[b] Put any functionality into a &lt;a href="http://www.phdcc.com/phdcc.CodeModule/"&gt;phdcc.CodeModule&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Much valuable functionality - jquery-ui / google maps / facebook / janrain / plupload / galleria - is available as javascript libraries. To get them onto a DNN Page, I have created a template CodeModule as follows.&lt;br /&gt;&lt;br /&gt;ASP.NET gives you limited control over where in the page javascript can be inserted - RegisterClientScriptBlock inserts somewhere near the top; RegisterStartupScript somewhere near the bottom. All I can say is - the template works on all browsers tested.&lt;br /&gt;&lt;br /&gt;In this example, I load the jquery-ui library at the top, and daisy chain the DNN onload handler at the bottom. The javascript could of course be located in a separate file.&lt;br /&gt;&lt;br /&gt;In the onload handler, I create an invisible empty jquery-ui dialog and initialise a jquery-ui accordion. I bind the submit button of a PHDCC form to a handler and examine one of the form fields to give feedback via the dialog.&lt;br /&gt;&lt;br /&gt;I add the CodeModule somewhere out of the way on the page - the bottom pane is a good place. Once there, accordions and dialogs can be used anywhere on the page.&lt;br /&gt;&lt;br /&gt;If you need any help with this or any of the suggested libraries, please get back to us.&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;&amp;lt;%@ Control Language="C#" ClassName="JCformWidgets" %&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;script runat="server"&amp;gt;&lt;br /&gt;&lt;br /&gt;//--------------------------------------&lt;br /&gt;private void loadJS()&lt;br /&gt;{&lt;br /&gt; string scriptKey = "JCjsIncludes:" + this.UniqueID;&lt;br /&gt; if( Page.IsStartupScriptRegistered( scriptKey) || Page.IsPostBack) return;&lt;br /&gt;&lt;br /&gt; ClientScriptManager cs = Page.ClientScript;&lt;br /&gt; Type cstype = this.GetType();&lt;br /&gt;&lt;br /&gt; // ----------------------------&lt;br /&gt; // load JS files at the top of the page&lt;br /&gt; // ----------------------------&lt;br /&gt; string scriptBlock = "\n";&lt;br /&gt;&lt;br /&gt; scriptBlock += "&amp;lt;link type='text/css' href='/jqui/jquery-ui-1.8.14.custom.css' rel='Stylesheet'&amp;gt;\n";&lt;br /&gt; scriptBlock += "&amp;lt;script src='/jqui/jquery-ui-1.8.14.custom.min.js' type='text/javascript'&amp;gt;";&lt;br /&gt; scriptBlock += "&amp;lt;/";&lt;br /&gt; scriptBlock += "script&amp;gt;\n";  // avoid microsoft parsing bug&lt;br /&gt;&lt;br /&gt; cs.RegisterClientScriptBlock( cstype, scriptKey, scriptBlock);&lt;br /&gt;&lt;br /&gt; // ----------------------------&lt;br /&gt; // load onload handler etc. at the bottom of the page&lt;br /&gt; // ----------------------------&lt;br /&gt; string startupScript = &lt;br /&gt;  @"&amp;lt;script language='JavaScript'&amp;gt;&lt;br /&gt;&amp;lt;!--&lt;br /&gt;&lt;br /&gt;function JConload(){&lt;br /&gt;&lt;br /&gt; var oldOnLoad = window.onload;&lt;br /&gt;&lt;br /&gt; var myDialog = $('&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;').dialog( {autoOpen: false} );&lt;br /&gt;   &lt;br /&gt; this.newOnload = function(){&lt;br /&gt;  &lt;br /&gt;  // call existing handler&lt;br /&gt;  if(typeof oldOnLoad === 'function'){ &lt;br /&gt;   oldOnLoad();&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;  // -------------------------------------&lt;br /&gt;&lt;br /&gt;  $(function() {&lt;br /&gt;   $( '#accordion' ).accordion();&lt;br /&gt;  });&lt;br /&gt;  &lt;br /&gt;  // -------------------------------------&lt;br /&gt;&lt;br /&gt;  $('#' + 'dnn_ctr422_ViewForm_btnSubmit').bind('click', function() {&lt;br /&gt;&lt;br /&gt;   var theseSkills = $( '#' + 'dnn_ctr422_ViewForm_Group_157_400_Question_157_400_1563_form_157_text_1563').val();&lt;br /&gt;   var thisDialogText = '';&lt;br /&gt; &lt;br /&gt;   if( theseSkills.length &amp;lt;= 0) {&lt;br /&gt;    thisDialogText = 'No skills given&amp;lt;br&amp;gt;Please try again.';&lt;br /&gt;&lt;br /&gt;   myDialog.html( thisDialogText);&lt;br /&gt;    myDialog.dialog( 'option', 'title', 'Things you forgot ...' );&lt;br /&gt;  &lt;br /&gt;    myDialog.dialog( 'open');&lt;br /&gt;    return false;&lt;br /&gt;   }&lt;br /&gt;  });&lt;br /&gt;&lt;br /&gt;  // -------------------------------------&lt;br /&gt;&lt;br /&gt; };&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;(function(){&lt;br /&gt; var JCOL = new JConload();&lt;br /&gt; window.onload = JCOL.newOnload;&lt;br /&gt;}());&lt;br /&gt;&lt;br /&gt;            // --&amp;gt;";&lt;br /&gt;&lt;br /&gt; startupScript += "&amp;lt;/sc";&lt;br /&gt; startupScript += "ript&amp;gt;"; // avoid microsoft parsing bug&lt;br /&gt;&lt;br /&gt; cs.RegisterStartupScript( cstype, scriptKey, startupScript);&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;//--------------------------------------&lt;br /&gt;protected void Page_Load(object sender, EventArgs e)&lt;br /&gt;{&lt;br /&gt; loadJS();&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&amp;lt;/script&amp;gt;&lt;br /&gt; &lt;br /&gt; &lt;br /&gt; &lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1126784059521484273?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1126784059521484273/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1126784059521484273' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1126784059521484273'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1126784059521484273'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2011/08/using-phdcc-codemodule-with-javascript.html' title='Using PHDCC CodeModule with javascript'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3885495399612717613</id><published>2011-05-12T11:17:00.000+01:00</published><updated>2011-05-13T21:36:26.957+01:00</updated><title type='text'>Scheduled task ping for plesk in Windows</title><content type='html'>I've been moving several domains to a Windows cloud hosting package at &lt;a href="http://www.webhosting.uk.com/"&gt;http://www.webhosting.uk.com/&lt;/a&gt;. I'm currently using Cloud Beginner hosting that gives 10 domains and a dedicated IP address for £12/month. This package lets us run DotNetNuke (DNN) sites - and fast too as there is not much contention on the included SQL Server databases. The cloud is UK based so the ping time is less from the UK and nearby.&lt;br /&gt;&lt;br /&gt;The account is run using Windows plesk which lets you control most things. However intervention is often needed from the webhosting.uk.com staff. They are usually very responsive and competent on their Live Chat service. Typical requests include setting ASP.NET modify permissions for the Network Service user and setting Application Starting Points. You need to insist that you get the dedicated IP address if you've asked for it. Email is included using the Horde web client, which isn't great; perhaps using Google Apps for email might be the best solution. No web stats package is included. Nor are file or database backups.&lt;br /&gt;&lt;br /&gt;For one domain, one task that I needed to do was to set up scheduled tasks to ping various URLs. While this could be done externally, I decided to use the Plesk scheduled task feature. When FTPing in, the main site is at /httpdocs/. The scheduled task VBS scripts should be placed in /cgi-bin/&lt;br /&gt;&lt;br /&gt;Using a couple of resources on the Internet, I put together this script, called &lt;span style="font-family:courier new;"&gt;pingurls.vbs&lt;/span&gt;:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;Call DoScheduledTask()&lt;br /&gt;&lt;br /&gt;Sub DoScheduledTask()&lt;br /&gt; On Error Resume Next&lt;br /&gt;&lt;br /&gt; Const ForReading = 1&lt;br /&gt;&lt;br /&gt; Set objFSO = CreateObject("Scripting.FileSystemObject")&lt;br /&gt; Set objFile = objFSO.OpenTextFile("E:\inetpub\vhosts\mydomainname.com\cgi-bin\urls2ping.txt", ForReading)&lt;br /&gt;&lt;br /&gt; Dim URL&lt;br /&gt; Do Until objFile.AtEndOfStream&lt;br /&gt;  URL = objFile.ReadLine&lt;br /&gt;  if Len(URL)&amp;gt;0 then PingURL(URL)&lt;br /&gt; Loop&lt;br /&gt; objFile.Close&lt;br /&gt;&lt;br /&gt;end sub&lt;br /&gt;&lt;br /&gt;sub PingURL(URL)&lt;br /&gt;&lt;br /&gt; Dim RequestObj&lt;br /&gt; Set RequestObj = CreateObject("Microsoft.XMLHTTP")&lt;br /&gt; 'Open request and pass the URL&lt;br /&gt; RequestObj.open "POST", URL , false&lt;br /&gt; 'Send Request&lt;br /&gt; RequestObj.Send&lt;br /&gt; 'cleanup&lt;br /&gt; Set RequestObj = Nothing&lt;br /&gt;&lt;br /&gt;End Sub&lt;/pre&gt;&lt;br /&gt;This script reads a text file &lt;span style="font-family:courier new;"&gt;urls2ping.txt&lt;/span&gt;, also in the /cgi-bin/ directory. Each line contains a URL to be pinged.&lt;br /&gt;&lt;br /&gt;How do you know what absolute path to use to get to the text file? This ASP.NET code, say in MyPath.aspx, will tell you the path it is running from:&lt;br /&gt;&lt;pre&gt;&amp;lt;%@ Page Language="VB" %&amp;gt;&lt;br /&gt;&amp;lt;script runat="server"&amp;gt;&lt;br /&gt;Sub Page_Load()&lt;br /&gt; lblPath.Text = Request.MapPath("~")&lt;br /&gt;End Sub&lt;br /&gt;&amp;lt;/script&amp;gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;head&amp;gt;&lt;br /&gt; &amp;lt;title&amp;gt;Web app path using Request.MapPath&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;lt;/head&amp;gt;&lt;br /&gt;&amp;lt;body&amp;gt;&lt;br /&gt; &amp;lt;asp:Label ID="lblPath" Runat="server" /&amp;gt;&lt;br /&gt;&amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;The final stage is to set up the scheduled task in plesk. This is straight forward using [Scheduled Tasks] then [Add New Task]. The Path to Executable file should be similar to that used above, ie &lt;span style="font-family:courier new;"&gt;E:\inetpub\vhosts\mydomainname.com\cgi-bin\pingurls.vbs&lt;/span&gt;. Set the times you want the task to run.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3885495399612717613?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3885495399612717613/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3885495399612717613' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3885495399612717613'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3885495399612717613'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2011/05/scheduled-task-ping-for-plesk-in.html' title='Scheduled task ping for plesk in Windows'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2765377585418045764</id><published>2010-11-20T09:14:00.012Z</published><updated>2010-11-20T15:34:47.803Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='VPS'/><category scheme='http://www.blogger.com/atom/ns#' term='Tomcat'/><category scheme='http://www.blogger.com/atom/ns#' term='Apache'/><category scheme='http://www.blogger.com/atom/ns#' term='Ubuntu'/><title type='text'>Installing Tomcat onto Ubuntu Apache2</title><content type='html'>&lt;p&gt;This post describes how to install Tomcat 6 on an Ubuntu 10.10 server and connect it up with the Apache2 server using the jk module. Although there are plenty of posts about this, none gave a full list of instructions; this is what I aim to provide - let me know if you have any corrections.&lt;/p&gt;&lt;p&gt;I assume that you have Apache2 installed.&lt;/p&gt;&lt;p&gt;You will need to do most of the following actions at a root shell prompt. Get this from your login using: &lt;code&gt;sudu su -&lt;/code&gt; You will need to edit various text files. You can do this using vi at the shell prompt. However I usually do this on my local computer using my preferred editor, transferring files to and fro using FTP. &lt;/p&gt;&lt;p&gt;&lt;strong&gt;Installing Tomcat&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This command gets Tomcat 6 - and Java if need be:&lt;br /&gt;&lt;code&gt;aptitude install tomcat6&lt;/code&gt;&lt;/p&gt;&lt;p&gt;I'm not sure if that gets all the Tomcat files, so enter this to get them all:&lt;br /&gt;&lt;code&gt;apt-get install tomcat6 tomcat6-admin tomcat6-common tomcat6-user tomcat6-docs tomcat6-examples&lt;/code&gt;&lt;/p&gt;&lt;p&gt;On my system, Java was installed to here: &lt;code&gt;/usr/lib/jvm/default-java/&lt;/code&gt;&lt;/p&gt;&lt;p&gt;You now need to set up Java environment variables in text script file &lt;code&gt;/etc/environment&lt;/code&gt;&lt;br /&gt;Add this to the PATH variable: &lt;code&gt;/usr/lib/jvm/default-java/bin&lt;/code&gt; after a colon separator. Add these lines:&lt;/p&gt;&lt;pre&gt;JAVA_HOME=/usr/lib/jvm/default-java&lt;br /&gt;JDK_HOME=/usr/lib/jvm/default-java&lt;/pre&gt;&lt;p&gt;You will now need to reboot so these changes are used.&lt;/p&gt;&lt;p&gt;Tomcat should now be running. You can start, stop or restart it as follows:&lt;/p&gt;&lt;pre&gt;/etc/init.d/tomcat6 stop&lt;br /&gt;/etc/init.d/tomcat6 status&lt;br /&gt;/etc/init.d/tomcat6 restart&lt;/pre&gt;&lt;p&gt;or more easily like this:&lt;/p&gt;&lt;pre&gt;service tomcat6 restart&lt;/pre&gt;&lt;p&gt;Tomcat should now be running on port 8080 at your host, eg http://www.example.com:8080/  If you go there, you should see an &lt;strong&gt;It works&lt;/strong&gt; page. To test it further, I put a JSP file in here: &lt;code&gt;/var/lib/tomcat6/webapps/ROOT/&lt;/code&gt; and saw that it ran OK. I then put a servlet WAR file in &lt;code&gt;/var/lib/tomcat6/webapps/&lt;/code&gt;. This was soon expanded automatically by Tomcat and I was able to navigate to the servlet directory (still on the 8080 port).&lt;/p&gt;&lt;p&gt;You may find it useful to run the &lt;strong&gt;manager&lt;/strong&gt; and &lt;strong&gt;host-manager&lt;/strong&gt; Tomcat applications. To enable these you need to edit the Tomcat configuration files in &lt;code&gt;/etc/tomcat6/&lt;/code&gt;. Edit &lt;code&gt;tomcat-users.xml&lt;/code&gt; to include the following:&lt;/p&gt; &lt;pre&gt;&amp;lt;role rolename="manager"/&amp;gt;&lt;br /&gt;&amp;lt;role rolename="admin"/&amp;gt;&lt;br /&gt;&amp;lt;user name="admin" password="secret_password" roles="manager,admin"/&amp;gt;&lt;/pre&gt;&lt;p&gt;You should now be able to access these apps at /manager/html and /host-manager/html. You will need to enter your credentials; possibly a couple of times on first access.&lt;/p&gt;&lt;p&gt;The Tomcat logs are at &lt;code&gt;/var/log/tomcat6/&lt;/code&gt;. By default Tomcat creates a different log file for each day. Keep an eye on these, and the disk space they consume.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Connecting Tomcat to Apache&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Hopefully you already know these commands to test your Apache configuration and restart Apache. You'll need these later.&lt;/p&gt;&lt;pre&gt;apache2ctl configtest&lt;br /&gt;apache2ctl restart&lt;/pre&gt;&lt;p&gt;First, tell Tomcat to listen out for connections from Apache. Edit &lt;code&gt;/etc/tomcat6/server.xml&lt;/code&gt; to uncomment this line: &lt;/p&gt;&lt;pre&gt;&amp;lt;Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /&amp;gt;&lt;/pre&gt;&lt;p&gt;Restart Tomcat. &lt;/p&gt;&lt;p&gt;Install the &lt;strong&gt;jk&lt;/strong&gt; module that acts as the connector between Apache and Tomcat:&lt;br /&gt;&lt;code&gt;apt-get install libapache2-mod-jk&lt;/code&gt;&lt;/p&gt;&lt;p&gt;You should see a file called &lt;code&gt;jk.load&lt;/code&gt; in &lt;code&gt;/etc/apache2/mods-available/&lt;/code&gt;. I edited this to contain:&lt;/p&gt;&lt;pre&gt;LoadModule jk_module /usr/lib/apache2/modules/mod_jk.so&lt;br /&gt;JkWorkersFile /etc/libapache2-mod-jk/workers.properties&lt;br /&gt;JkLogFile /var/log/apache2/mod_jk.log&lt;br /&gt;JkShmFile /var/log/apache2/jk-runtime-status&lt;br /&gt;JkLogLevel    info&lt;/pre&gt;&lt;p&gt;You can also add JkMount instructions here, but I found that I needed to put them in each VirtualHost file in &lt;code&gt;/etc/apache2/sites-available/&lt;/code&gt;. Eg in my &lt;code&gt; default&lt;/code&gt; VirtualHost file I added:&lt;/p&gt;&lt;pre&gt;JkMount /*.jsp ajp13_worker&lt;br /&gt;JkMount /manager ajp13_worker&lt;br /&gt;JkMount /manager/* ajp13_worker&lt;br /&gt;JkMount /host-manager ajp13_worker&lt;br /&gt;JkMount /host-manager/* ajp13_worker&lt;/pre&gt;&lt;p&gt;An alternative is to define JkMount commands in &lt;code&gt;jk.load&lt;/code&gt; and add a JkMountCopy command to copy these settings to all virtual hosts.&lt;/p&gt;&lt;p&gt;You now need to edit the jk worker file &lt;code&gt;/etc/libapache2-mod-jk/workers.properties&lt;/code&gt;. Make sure it has code as follows, ie to tell jk to use the Tomcat connection we defined earlier:&lt;/p&gt;&lt;pre&gt;worker.list=ajp13_worker&lt;br /&gt;worker.ajp13_worker.port=8009&lt;br /&gt;worker.ajp13_worker.host=localhost&lt;br /&gt;worker.ajp13_worker.type=ajp13&lt;/pre&gt;&lt;p&gt;Restart apache.&lt;/p&gt;&lt;p&gt;I was now able to access the Tomcat manager here: http://www.example.com/manager/html.&lt;/p&gt;&lt;p&gt;Edit your other VirtualHost files to expose any other apps that you need.&lt;/p&gt;&lt;p&gt;You'll find the Apache and jk logs in here: &lt;code&gt;/var/log/apache2/&lt;/code&gt;. As above, keep an eye on these files and the disk space they use. &lt;/p&gt;&lt;p&gt;If you see "missing uri map" in log file &lt;code&gt;mod_jk.log&lt;/code&gt; then you need to define JkMount in your VirtualHost.&lt;/p&gt;&lt;p&gt;As a final task, if you wish, disable the Tomcat 8080 port by editing &lt;code&gt;/etc/tomcat6/server.xml&lt;/code&gt; and then restart Tomcat.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2765377585418045764?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2765377585418045764/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2765377585418045764' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2765377585418045764'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2765377585418045764'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/11/installing-tomcat-onto-ubuntu-apache2.html' title='Installing Tomcat onto Ubuntu Apache2'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-4841039057182551464</id><published>2010-09-21T17:29:00.008+01:00</published><updated>2010-11-08T17:05:19.375Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='Windows'/><title type='text'>Windows 7 system drive letters</title><content type='html'>&lt;p&gt;I've previously blogged about &lt;a href="http://chriscant.phdcc.com/2010/01/wandering-drive-letters-in-windows.html"&gt;Wandering drive letters in Windows 7&lt;/a&gt;.  I've now a little more hard-won knowledge of what's going on.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Be very careful if you resize a Windows 7 system partition when the partition letter is not C.  In fact, don't do it!&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I am revisiting this issue because I ran into a problem.  Our family shared laptop came with Vista.  However this started playing up showing a DOS box regularly and Windows Update wouldn't install a particular patch.  So I decided to install Windows 7 on drive D.  This worked fine with W7 running with its system drive as D:.&lt;/p&gt;&lt;p&gt;Recently, drive D: started getting full so I decided to resize the partitions with the great &lt;a href="http://www.partitionwizard.com/"&gt;Partition Wizard&lt;/a&gt;.  This seemed to go OK.  However, Windows 7 started playing up very badly, showing DwmHintDxUpdate error messages and worse.  I eventually tracked the problem down to the fact that Windows 7 had decided that drive D: would now be called drive C: which is a serious problem as Windows and other software will store many full file locations (in the registry and elsewhere).  If W7 thinks a file should be on drive D when in fact it is on drive C, then there's going to be serious problems.  It's amazing that it booted at all.&lt;/p&gt;&lt;p&gt;The post by MarcusOS7 &lt;a href="http://social.technet.microsoft.com/Forums/en-US/w7itproinstall/thread/47bcc58e-9792-409a-889f-796a07746a5e"&gt;here&lt;/a&gt; explained what was going on, ie that Windows 7 tries to rename its system partition to drive C if the drive is resized, ie when the drive identifier changes.  &lt;strong&gt;This is a serious flaw in Windows 7 and must be fixed.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I had hoped that with this information, I would be able to mend my W7 system.  Even though I could just run regedit, I did not know what magic to do to change the system drive letter.&lt;/p&gt;&lt;p&gt;Eventually I decided to reinstall Windows 7.  I saved the data over to an external USB drive.  For this installation I wanted to be sure that W7 would install itself on drive C.  Previously I must have done the installation when running the Vista system which somehow prevented W7 from using the C drive.  This time, I was careful to do the install after booting into the Windows DVD.  This trick ensured that W7 marked its install partition as drive C (even though it was not on the first partition).&lt;/p&gt;&lt;p&gt;In fact: before doing this, I re-partitioned the system so I had 3 partitions in the laptop, one each for Vista and W7, and one as a data drive.  This process also helped me clear the previous W7 installation which was quite stubborn to remove, permission-wise.  I eventually had to clear the partition by reformatting the drive.&lt;/p&gt;&lt;p&gt;Back in the new W7 system, I was able to restore the desired user data from the backup.  And then I had to set up W7 again from scratch for each user, and installing all my development tools.  As usual one of my first tasks is to turn off various system sounds, particularly the annoying Start Navigation click...&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-4841039057182551464?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/4841039057182551464/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=4841039057182551464' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4841039057182551464'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4841039057182551464'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/09/windows-7-system-drive-letters.html' title='Windows 7 system drive letters'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-6068217429513897780</id><published>2010-08-25T22:21:00.002+01:00</published><updated>2010-08-25T22:35:49.162+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Drupal'/><title type='text'>Drupal staging site security</title><content type='html'>Suppose you have a Drupal staging site where you are preparing to go live or testing new features. You could install this on a separate domain that you have. Here's how to redirect casual users to your live site, while giving those in the know easy entry.&lt;br /&gt;&lt;br /&gt;The crucial trick is to use a Session variable to indicate an authorised user. All Drupal access is via the root index.php file (except use of static files). index.php is amended to redirect users who do not have the session variable set correctly. Another secret file eg password.php, is used to let you get into the site by setting the session variable.&lt;br /&gt;&lt;br /&gt;In the following example, www.example.com is your live domain and www.example.info is the staging server.  The following code is on the staging server.&lt;br /&gt;&lt;br /&gt;In index.php add this code after the line that contains drupal_bootstrap...&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;if( $_SESSION['password']!='asecret')&lt;br /&gt;{&lt;br /&gt;  header('Location: http://www.example.com/');&lt;br /&gt;  exit;&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Create a secret file in the root directory eg password.php with content like this:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&amp;lt;?php&lt;br /&gt;&lt;br /&gt;require_once './includes/bootstrap.inc';&lt;br /&gt;drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);&lt;br /&gt;&lt;br /&gt;if( $_SESSION['password']=='asecret')&lt;br /&gt;{&lt;br /&gt;  header('Location: http://www.example.info/');&lt;br /&gt;  die();&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;$pwd = trim($_POST['pwd']);&lt;br /&gt;if( get_magic_quotes_gpc())&lt;br /&gt;{&lt;br /&gt; $pwd = stripslashes($pwd);&lt;br /&gt;}&lt;br /&gt;if( $pwd=='asecret')&lt;br /&gt;{&lt;br /&gt;  $_SESSION['password'] = $pwd;&lt;br /&gt;  header('Location: http://www.example.info/');&lt;br /&gt;  die();&lt;br /&gt;}&lt;br /&gt;?&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;html&amp;gt;&lt;br /&gt;&amp;lt;body&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;form method="post"&amp;gt;&lt;br /&gt;&lt;br /&gt;Security:&lt;br /&gt;&amp;lt;input type="text" name="pwd" /&amp;gt;&lt;br /&gt;&amp;lt;input type="submit" value="Go" /&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/form&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/body&amp;gt;&lt;br /&gt;&amp;lt;/html&amp;gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-6068217429513897780?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/6068217429513897780/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=6068217429513897780' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6068217429513897780'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6068217429513897780'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/08/drupal-staging-site-security.html' title='Drupal staging site security'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-8165235773973049577</id><published>2010-04-23T13:59:00.002+01:00</published><updated>2010-04-23T14:09:40.473+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='maps'/><category scheme='http://www.blogger.com/atom/ns#' term='Google'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Tool to find maps KML Lat/Lng</title><content type='html'>Use this tool if you want to find a Google Maps KML GLatLng location, eg to use within your own code: &lt;a href="http://www.phdcc.com/GoogleLatLng.htm"&gt;http://www.phdcc.com/GoogleLatLng.htm&lt;/a&gt;  Either drag the initial marker or enter a search term.  The latitude and longitude values are listed below, to 6 decimal places of accuracy.&lt;br /&gt;&lt;br /&gt;The map is centred on the UK, as this is where I will want to use it most.  It uses the Google AJAX Search API to work out UK postcodes well.  Look at the source to see how it works.&lt;br /&gt;&lt;br /&gt;This is based on the code by cmarshall at &lt;a href="http://www.webmasterworld.com/xml/3542700.htm"&gt;http://www.webmasterworld.com/xml/3542700.htm&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-8165235773973049577?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/8165235773973049577/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=8165235773973049577' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8165235773973049577'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8165235773973049577'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/04/tool-to-find-maps-kml-latlng.html' title='Tool to find maps KML Lat/Lng'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2519841563173039500</id><published>2010-04-19T12:45:00.005+01:00</published><updated>2010-04-19T13:03:54.486+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN5 User soft-delete issues</title><content type='html'>In DotNetNuke DNN5 when you delete a user that login is no longer removed from the system.  Instead they are soft-deleted, ie a new IsDeleted flag column is set in the UserPortals table. (Note that IsDeleted in the Users table is *not* set - is this ever set?)&lt;br /&gt;&lt;br /&gt;In DNN5, code that calls DotNetNuke.Entities.Users.UserController.GetUser() etc will return a UserInfo object even if the user is deleted.  Therefore you may have to check the UserInfo.IsDeleted property every time you get a user.&lt;br /&gt;&lt;br /&gt;I would have not have implemented it this way.  I'd keep the DNN4 functionality and have extra API calls to find deleted users.  I wonder: does the DNN core code always check IsDeleted now?&lt;br /&gt;&lt;br /&gt;Anyway, UserInfo.IsDeleted is not available in DNN4.  As I do not want different versions of my code for DNN4 and DNN5, I have written this isUserDeleted() static method that uses reflection to detect if the IsDeleted property is available and if so calls it.  It returns true if the UserInfo is null.  After that, for DNN4 it always returns false.&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;public static bool isUserDeleted(UserInfo ui)&lt;br /&gt;{&lt;br /&gt; if (ui == null) return true;&lt;br /&gt; try          {&lt;br /&gt;  Type tUserInfo = ui.GetType();&lt;br /&gt;&lt;br /&gt;  PropertyInfo piIsDeleted = tUserInfo.GetProperty("IsDeleted");&lt;br /&gt;  if (piIsDeleted != null)&lt;br /&gt;  {&lt;br /&gt;   bool IsDeleted = (bool)piIsDeleted.GetValue(ui, null);&lt;br /&gt;   return IsDeleted;&lt;br /&gt;  }&lt;br /&gt; }&lt;br /&gt; catch (Exception) { }&lt;br /&gt; return false;&lt;br /&gt;}&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2519841563173039500?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2519841563173039500/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2519841563173039500' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2519841563173039500'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2519841563173039500'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/04/dnn5-user-soft-delete-issues.html' title='DNN5 User soft-delete issues'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-8488635505539581864</id><published>2010-04-10T09:00:00.021+01:00</published><updated>2010-04-14T17:09:09.445+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='Google'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><category scheme='http://www.blogger.com/atom/ns#' term='Freegle'/><title type='text'>Freegle Find a Group mashup</title><content type='html'>&lt;p&gt;This blog describes how I've used one JavaScript script to power several different incarnations of a Google Maps tool to help people Find a Freegle Group.  Usage instructions for the Find a Group tool are on the &lt;a href="http://wiki.ilovefreegle.org/Find_a_Group_maps"&gt;Freegle wiki&lt;/a&gt;.  You can also see the Find a Group gadger lower down on this page on the right.&lt;/p&gt;&lt;p&gt;&lt;b&gt;This is primarily a technical article - for usage instructions, see the above link.&lt;/b&gt;&lt;p&gt;&lt;img src="http://maps.iLoveFreegle.org/freegle_gadget_screenshot_280x246.png" alt="Freegle Find a Group screenshot" title="Freegle Find a Group screenshot" style="float:right;padding:2px;border:1px;" /&gt;&lt;a href="http://www.ilovefreegle.org/"&gt;Freegle&lt;/a&gt; is a network of local re-use groups in the UK.  Group members give away and receive any unwanted items for free.  This helps conserve the world's resources, clears clutter and helps others.&lt;/p&gt;&lt;p&gt;The Find a Group tool shows a UK map with pinpoint markers for each Freegle group.  You can move around and zoom in as normal - clicking on a marker shows an information bubble - clicking on the link in the bubble takes you to the group web site.&lt;/p&gt;&lt;p&gt;Find a Group also has a search box.  The tool uses Google to search for that location in the UK.  If found, the tool zooms to that location and adds a marker - you are invited to look for the nearest Freegle group marker.  The nearest groups are listed below&lt;/p&gt;&lt;h2&gt;Source data&lt;/h2&gt;&lt;p&gt;The green group markers come from &lt;a href="http://maps.google.co.uk/maps/ms?hl=en&amp;ie=UTF8&amp;oe=UTF8&amp;t=p&amp;msa=0&amp;msid=104032218577461097800.000478fbc5161363404bd&amp;output=kml"&gt;this KML file&lt;/a&gt; - maintained by another Freegle member.  This is the latest data so the tool should always be up to date.&lt;/p&gt;&lt;p&gt;Actually, that's not the whole truth.  The tool needs to load the KML file into memory so it can search for groups.  Loading the above Google Maps KML link doesn't work because of cross-site scripting security (as I understand it).  Therefore the code loads a separate snapshot of the KML file from a URL that it can access.&lt;/p&gt;&lt;p&gt;There's a plan to resolve this issue by having the primary KML file on the iLoveFreegle.org servers.  That way, my code would only need to reference one file.  And the plan is to have this file generated directly from the main Freegle groups database, so it is always up to date.  (Currently the Google Maps KML file has to be updated by hand.)&lt;/p&gt;&lt;h2&gt;Mashup variants&lt;/h2&gt; &lt;h3&gt;(P) Full web page&lt;/h3&gt;&lt;p&gt;The Find a Group tool occupies a full web page here: &lt;a href="http://maps.ilovefreegle.org/"&gt;maps.ilovefreegle.org&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;(M) Google Mapplet&lt;/h3&gt;&lt;p&gt;The Find a Group tool appears in the Google mapplets directory &lt;a href="http://maps.google.co.uk/gadgets/directory?synd=mpl&amp;hl=en&amp;gl=en&amp;url=http%3A%2F%2Fmaps.ilovefreegle.org%2Ffreegle_mapplet.xml"&gt;here&lt;/a&gt;.  When added to My Maps it shows filling the browser window.&lt;/p&gt;&lt;h3&gt;(I) Iframe&lt;/h3&gt;&lt;p&gt;The Iframe version of the Find a Group tool is designed to fit into a small window within another page.  The &lt;a href="http://wiki.ilovefreegle.org/Find_a_Group_maps#IFrame"&gt;usage instructions&lt;/a&gt; describe the various configuration parameters.&lt;/p&gt;&lt;p&gt;You can see the iframe in action at the &lt;a href="http://groups.yahoo.com/group/PenrithEdenFreegle/"&gt;Penrith and Eden District Freegle group&lt;/a&gt; and its associated &lt;a href="http://www.penrithedenfreegle.org.uk/"&gt;helper web site&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;(G) Google gadget&lt;/h3&gt;&lt;p&gt;The Find a Group tool is packaged as a Google Gadget, &lt;a href="http://maps.ilovefreegle.org/freegle_gadget.xml"&gt;defined here&lt;/a&gt;.  You can therefore add it to iGoogle, add it to a blog, and get the code to put on a web site.  The gadget has similar options to the iframe.&lt;/p&gt;&lt;p&gt;The gadget has a Content type of url, with the main gadget code &lt;a href="http://maps.iLoveFreegle.org/freegle_gadget.php"&gt;here&lt;/a&gt;.  The gadget options are passed as URL QueryString parameters.&lt;/p&gt;&lt;h3&gt;(F) Facebook application&lt;/h3&gt;&lt;p&gt;The Find a Group tool is being developed as a simple Facebook iframe application.&lt;/p&gt;&lt;h2&gt;Common Page Layout&lt;/h2&gt;&lt;p&gt;Each of the above tools has HTML which has the same page elements:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Map div called "map_canvas"&lt;/li&gt;&lt;li&gt;Noscript block&lt;/li&gt;&lt;li&gt;Div called "jsBlock" that wraps the search form&lt;/li&gt;&lt;li&gt;Input text box called "search"&lt;/li&gt;&lt;li&gt;Submit button that has an onclick handler "return doSearch();" that always returns false&lt;/li&gt;&lt;li&gt;Information div called "idInfo"&lt;/li&gt;&lt;li&gt;Debug div called "idDebug" that shows version info by default&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Accessibility&lt;/h2&gt;&lt;p&gt;The tool has a noscripts block that has a link to the main site groups list - this is shown if the browser has no JavaScript or it is turned off.  However Chrome doesn't show this text if JavaScript is turned off - sounds like a bug to me.&lt;/p&gt;&lt;p&gt;The "jsBlock" search form div is not displayed by default; the JavaScript makes this block visible.  This avoids confusion if JavaScript is not available.&lt;/p&gt;&lt;p&gt;The search text box has an accesskey of "4" defined.  For the larger variants, a label is defined for this text box.&lt;/p&gt;&lt;p&gt;The tool HTML is hopefully valid XHTML and CSS.&lt;/p&gt;&lt;h2&gt;API and api keys&lt;/h2&gt;&lt;p&gt;The Google Maps/Mapplets API is used in the JavaScript.  There are various differences depending on whether it is a mapplet or not.  Therefore one of the JavaScript initialise() parameters is a mapplet boolean; this is stored in a global for later use throughout the code.&lt;/p&gt;&lt;p&gt;To use the Google Maps API, each site needs an API key.  All these tools run on the maps.iLoveFreegle.org domain so the iLoveFreegle.org API key is used.  The mapplet code does not need an API key.&lt;/p&gt;&lt;h2&gt;JavaScript structure&lt;/h2&gt;&lt;p&gt;The JavaScript is pretty straightforward procedural code, with some globals, an &lt;i&gt;initialise()&lt;/i&gt; function and a function &lt;i&gt;doSearch()&lt;/i&gt; that is called when a user does a search.  The JavaScript code is here: &lt;a href="http://maps.ilovefreegle.org/freegle_maps.js"&gt;http://maps.ilovefreegle.org/freegle_maps.js&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;initialise() function&lt;/h3&gt;&lt;p&gt;This takes parameter specifying whether big map controls are wanted, and the mapplet boolean.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If not a mapplet, check that the browser is Google Maps compatible&lt;/li&gt;&lt;li&gt;Get references to the "idInfo" and "idDebug" page elements - add the js version to idDebug&lt;/li&gt;&lt;li&gt;If the "jsBlock" page element is present, change its display style to block to make it show.&lt;/li&gt;&lt;li&gt;Get the map object and set the map height if it has been specified in a parameter.&lt;/li&gt;&lt;li&gt;Get the z SearchZoom integer parameter for later use&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Set the map to be the approx centre of the UK at a suitable scale&lt;/li&gt;&lt;li&gt;Use GGeoXml to load the primary KML file and add it as an overlay - the code cannot access the contents of the overlay, so...&lt;/li&gt;&lt;li&gt;Initiate a separate load of the (secondary) KML for processing, either using _IG_FetchXmlContent() or GDownloadUrl()&lt;/li&gt;&lt;li&gt;Finally set the focus to the search box if required&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;processKML() function&lt;/h3&gt;&lt;p&gt;This parses the received KML XML file and stores info about each Group placemark object in the Groups array.&lt;/p&gt;&lt;p&gt;Note that the KML file stores each coordinate the wrong way round, ie longitude, latitude then height.  The GLatLng.fromUrlValue() function needs just the first two values but reversed.&lt;/p&gt;&lt;p&gt;Once the KML is loaded, it sees if a search has been requested and does it.&lt;/p&gt;&lt;h3&gt;doSearch() function&lt;/h3&gt;&lt;p&gt;The search function does a Google GClientGeocoder search and zooms in to show the result.  The search also lists the nearest groups, calculated using GLatLng.distanceFrom().&lt;/p&gt;&lt;p&gt;doSearch() uses showAddressResult(), SearchGroups() and createMarker().&lt;/p&gt;&lt;h3&gt;Helper functions&lt;/h3&gt;&lt;p&gt;String.prototype.trim, PageQuery, queryString() and isInteger() were obtained from code on the Internet.  I added queryString2() helper function.&lt;/p&gt;&lt;p&gt;I have updated PageQuery to cope with URL parameters with key=value pairs where "=value" is missing.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-8488635505539581864?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/8488635505539581864/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=8488635505539581864' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8488635505539581864'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8488635505539581864'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/04/freegle-find-group-mashup.html' title='Freegle Find a Group mashup'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-7869338740635118680</id><published>2010-03-19T19:41:00.007Z</published><updated>2010-03-19T19:49:54.445Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='Vista'/><title type='text'>800700C1 KB977165 and verclsid</title><content type='html'>Our Vista laptop has developed 3 problems recently, which are probably all related.  Can anyone help?&lt;br /&gt;&lt;br /&gt;Windows Update keeps on trying to install KB977165, sometimes successfully, but it keeps re-appearing.&lt;br /&gt;&lt;br /&gt;Clicking on Check for Updates now fails with error 800700C1.&lt;br /&gt;&lt;br /&gt;It also often shows an error with a DOS box title C:\Windows\system32\verclsid.exe and a message 16 bit MS-DOS Subsystem saying The NTVDM CPU has encountered an illegal instruction.&lt;br /&gt;&lt;br /&gt;We did have some software for 2 HP printers installed.  I've uninstalled one of them as this was suggested as being a problem.&lt;br /&gt;&lt;br /&gt;Any other ideas pls?&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S6PU1MtMI0I/AAAAAAAAAMY/-yJpE2Nv8u4/s1600-h/WindowsVistaKB977165.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 200px; height: 88px;" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S6PU1MtMI0I/AAAAAAAAAMY/-yJpE2Nv8u4/s200/WindowsVistaKB977165.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433984457352002" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S6PU1dJGUSI/AAAAAAAAAMg/O_Fg7nUaaeA/s1600-h/WindowsUpdate800700C1.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 200px; height: 112px;" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S6PU1dJGUSI/AAAAAAAAAMg/O_Fg7nUaaeA/s200/WindowsUpdate800700C1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433988869378338" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_hkkBsHR0r5s/S6PU1j8XNLI/AAAAAAAAAMo/tcXZ4sN0dHY/s1600-h/NTVDMerror2.jpg"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 200px; height: 102px;" src="http://3.bp.blogspot.com/_hkkBsHR0r5s/S6PU1j8XNLI/AAAAAAAAAMo/tcXZ4sN0dHY/s200/NTVDMerror2.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433990695007410" /&gt;&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-7869338740635118680?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/7869338740635118680/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=7869338740635118680' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7869338740635118680'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7869338740635118680'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/03/800700c1-kb977165-and-verclsid.html' title='800700C1 KB977165 and verclsid'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_hkkBsHR0r5s/S6PU1MtMI0I/AAAAAAAAAMY/-yJpE2Nv8u4/s72-c/WindowsVistaKB977165.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-988104417504935403</id><published>2010-03-09T10:42:00.003Z</published><updated>2010-03-10T20:49:44.769Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>Crystaltech to 3essentials DNN copy</title><content type='html'>&lt;p&gt;PHDCC Director John Cant describes the process of transferring a DNN site from one shared host to another, at this stage simply as a backup using a different/private domain.&lt;/p&gt;&lt;p&gt;The site, ourdnnsite.com is being copied from Newtek (Crystaltech) hosting to 3Essentials with domain ourdnnsite.info. Both support services are very helpful but it currently seems that 3essentials runs faster than Crystaltech, probably because of a faster database server.&lt;/p&gt;&lt;p&gt;The existing site runs only one portal but this contains many users and many files in /Portals/0/. Maintaining the UserIDs is crucial as much custom information in the database is keyed off this value.&lt;/p&gt;&lt;p&gt;&lt;a href="http://knowledge.3essentials.com/web-hosting/article/369/What-ASP.NET-versions-1.1-2.0-3.5-does-3Essentials-support-and-how-do-I-enable-change-versions.html"&gt;Enable ASP.NET 3.5 at the 3essentials site.&lt;/a&gt;&lt;/p&gt;&lt;p&gt;First, place a holding page at ourdnnsite.info, eg index.html, that will be shown instead of Default.aspx for people accessing the root domain URL.&lt;/p&gt;&lt;p&gt;Make sure to truncate transaction log prior to the upgrade, clear the eventlog and maybe even sitelog and search tables, if those are exaggeratingly large.&lt;/p&gt;&lt;p&gt;The 3essentials site currently runs SQL Server 2005/2003. They need a .BAK backup file so that the database copy can be restored correctly.&lt;/p&gt;&lt;p&gt;Do a database backup on the Crystaltech site and ask for the entire site to be zipped, to get these files:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ourdnnsitedb_1002050754.bak&lt;/li&gt;&lt;li&gt;ourdnnsite.zip&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;p&gt;Load these to the 3essentials site ourdnnsite.info /private/&lt;/p&gt;&lt;p&gt;Submit a 3essentials site support request, asking for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the database to be restored from /private/ourdnnsitedb_1002050754.bak&lt;/li&gt;&lt;li&gt;the site files to be unzipped from /private/ourdnnsite.zip&lt;/li&gt;&lt;li&gt;the site file permissions to be set correctly:&lt;br /&gt;"I have corrected this issue, this involves giving the httpdocs directory IWPD User - Modify permissions which you are not able to do via Plesk."&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Check the Web.Config file at the 3essentials site:&lt;/p&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;set the new database connection string twice&lt;/li&gt;&lt;li&gt;check the ObjectQualifier&lt;/li&gt;&lt;li&gt;set CustomErrors to 'Off' to see the errors&lt;/li&gt;&lt;li&gt;ensure the machinekey values are the same as the original values from the Crystaltech site&lt;/li&gt;&lt;li&gt;set AutoUpgrade off&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;On the database:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Update the portal alias, eg:&lt;br /&gt;&lt;code&gt;UPDATE [DNN_PortalAlias] SET&lt;br /&gt;HTTPAlias='www.ourdnnsite.info' WHERE PortalAliasID=0&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Access the site by visiting Default.aspx&lt;/p&gt;&lt;p&gt;&lt;br /&gt;If any errors are visible, keep deleting the sections of Web.Config that cause errors.&lt;/p&gt;&lt;p&gt;If you cannot log in, use Chris' technique, &lt;a href="http://knowledge.3essentials.com/web-hosting/article/376/Reset-DNN-host-password.html"&gt;described here&lt;/a&gt;. To access the DNN database, log in using myLittleAdmin here: &lt;a href="http://db1.3essentials.com/mla/"&gt;http://db1.3essentials.com/mla/&lt;/a&gt;&lt;/p&gt;&lt;p&gt;To prevent unauthorised access to the ourdnnsite.info site, add to the Default.aspx file a check that a cookie is set. Provide a means of setting the cookie using another private page.&lt;/p&gt;&lt;p&gt;There are a couple of other additions that we usually make:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;As well as creating an app_offline.htm file, we use the Cookie technique in Default.aspx to ensure that the site is offline during maintenance, redirecting to an offline page.&lt;/li&gt;&lt;li&gt;In the skin, we warn people who are online that the site is about to go down; this also helps us see if anyone is online now.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;More details on these Default.aspx techniques later if you wish.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-988104417504935403?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/988104417504935403/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=988104417504935403' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/988104417504935403'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/988104417504935403'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/03/crystaltech-to-3essentials-dnn-copy.html' title='Crystaltech to 3essentials DNN copy'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-9156108082597503727</id><published>2010-02-24T12:02:00.005Z</published><updated>2010-02-24T12:17:41.425Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='.NET ASP.NET'/><title type='text'>.NET app using Jet on x64</title><content type='html'>If you are trying to use an Access MDB file using the Microsoft Jet OleDb driver in .NET or ASP.NET, you must set your platform target to x86 (32 bit).&lt;br /&gt;&lt;br /&gt;If you are running on a 64 bit system and your assembly target is Any CPU or x64 then you will see this exception:&lt;br /&gt;&lt;em&gt;The 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine. &lt;/em&gt;&lt;br /&gt;&lt;br /&gt;The Microsoft ACE.OLEDB driver for MDB and ACCDB files works OK whichever platform is targeted.  However this driver is not installed by default on user computers unlike the Jet driver.&lt;br /&gt;&lt;br /&gt;This fix can be used to make existing code work with Jet/MDB, eg the Cassini server.&lt;br /&gt;&lt;br /&gt;Useful blog links:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;&lt;a href="http://www.lostechies.com/blogs/gabrielschenker/archive/2009/10/21/force-net-application-to-run-in-32bit-process-on-64bit-os.aspx"&gt; Force .NET application to run in 32bit process on 64bit OS&lt;/a&gt; by Gabriel Schenker&lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;a href="http://www.hanselman.com/blog/BackToBasics32bitAnd64bitConfusionAroundX86AndX64AndTheNETFrameworkAndCLR.aspx"&gt;Back to Basics: 32-bit and 64-bit confusion around x86 and x64 and the .NET Framework and CLR&lt;/a&gt; by Scott Hanselman&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-9156108082597503727?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/9156108082597503727/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=9156108082597503727' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9156108082597503727'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9156108082597503727'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/02/net-app-using-jet-on-x64.html' title='.NET app using Jet on x64'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-4581709963362320971</id><published>2010-02-22T14:04:00.006Z</published><updated>2010-02-22T15:07:29.799Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='.net string UTF8'/><title type='text'>System.String hidden UTF8 BOM</title><content type='html'>In .NET, a string (System.String) can contain an initial UTF-8 Byte Order Mark (BOM) which might not be seen in ordinary processing but is present when converted to a character array or into an encoding byte array.&lt;br /&gt;&lt;br /&gt;For example, a text file might be saved in UTF8 format with UTF-8 Byte Order Mark bytes at the start, ie 0xEF 0xBB 0xBF.  You might receive this file in ASP.NET using a FileUpload control, or read it directly in a Forms .NET app in C#:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;byte[] FileBytes = File.ReadAllBytes(path);&lt;br /&gt;string content = Encoding.UTF8.GetString(FileBytes);&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;If the file contains these 7 bytes (in hex) EF BB BF 44 65 61 72 then content will superficially contain the single word "Dear", eg as seen in the debugger, and content.StartsWith("Dear") will return true.&lt;br /&gt;&lt;br /&gt;However, content.Length is 5 and content.ToCharArray() will return an array with 5 elements, the first being set to 0xFEFF.  Similarly, Encoding.UTF8.GetBytes(content) will return the same 7 bytes as was used in the first place.&lt;br /&gt;&lt;br /&gt;(Note that that has nothing to do with the encoderShouldEmitUTF8Identifier optional parameter for the UTF8Encoding constructor.)&lt;br /&gt;&lt;br /&gt;As this hidden extra character can be misleading, I have written the following snippet that detects the presence of the UTF8 Byte Order Mark preamble and ignores it if present:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;byte[] FileBytes = File.ReadAllBytes(path);&lt;br /&gt;int StartPoint = 0;&lt;br /&gt;int Count = FileBytes.Length;&lt;br /&gt;if ( Count&gt;= 3 &amp;&amp; FileBytes[0] == 0xEF &amp;&amp; FileBytes[1] == 0xBB &amp;&amp; FileBytes[2] == 0xBF)&lt;br /&gt;{&lt;br /&gt; StartPoint += 3;&lt;br /&gt; Count -= 3;&lt;br /&gt;}&lt;br /&gt;content = Encoding.UTF8.GetString(FileBytes, StartPoint, Count);&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;PS  The code could not doubt be improved using Encoding.GetPreamble()&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-4581709963362320971?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/4581709963362320971/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=4581709963362320971' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4581709963362320971'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4581709963362320971'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/02/systemstring-hidden-utf8-bom.html' title='System.String hidden UTF8 BOM'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-8206748054448756984</id><published>2010-02-07T12:59:00.006Z</published><updated>2010-02-07T14:01:08.727Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='CSS'/><title type='text'>Clearing nested CSS floats</title><content type='html'>Here's a XHTML/CSS technique to ensure you only clear the desired floats and get the right background areas for your DIV.&lt;br /&gt;&lt;br /&gt;The code examples are here: &lt;a href="http://www.phdcc.com/CSS_ClearingFloats.htm"&gt;http://www.phdcc.com/CSS_ClearingFloats.htm&lt;/a&gt; - look at the source code for full details.&lt;br /&gt;&lt;br /&gt;My starting point is to have a DIV set with floats left and right.  Within the middle unfloated DIV, I have another DIV set with floats and right.  I want to clear just the middle floats, not the outer ones.&lt;br /&gt;&lt;br /&gt;The crucial trick is to add CSS style 'overflow:hidden' to create a Block Formatting Context.  Any CSS style 'clear:both' then only applies to the 'nearest'/current Block Formatting Context.&lt;br /&gt;&lt;br /&gt;An associated problem was the fact that the background of the middle unfloated DIV goes wide to the border of the current Block Formatting Context.  Adding CSS style 'overflow:hidden' to create a new context constrains the background to the expected area.&lt;br /&gt;&lt;br /&gt;Using 'overflow:hidden' feels slightly naff: shouldn't there be an explicit CSS style to define a Block Formatting Context, instead of using something that does this as a side effect?  Also, you may be concerned about what overflow is being hidden - as long as you have the default width:auto and/or height:auto set, you will not be hiding anything.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-8206748054448756984?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/8206748054448756984/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=8206748054448756984' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8206748054448756984'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8206748054448756984'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/02/clearing-nested-css-floats.html' title='Clearing nested CSS floats'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1593332343396110508</id><published>2010-02-05T15:01:00.014Z</published><updated>2010-02-05T16:01:00.066Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='accessibility'/><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>Adding Access key support to DotNetNuke</title><content type='html'>This page explains how to add "access key" support to a DNN5 web site.&lt;br /&gt;Access keys provide keyboard short-cuts to improve web accessibility, as described here - which also lists the UK government recommendations:&lt;br /&gt;&lt;a href="http://en.wikipedia.org/wiki/Access_key" title="Access key keyboard short-cuts"&gt;http://en.wikipedia.org/wiki/Access_key&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;I have implemented access keys using the accesskey attribute rather than the&lt;br /&gt;&lt;a href="http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-role.html#s_rolemodule"&gt;Role Access Model&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;How it works for a user&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;For standard users, each browser uses different modifier keys and acts minorly differently when pressed.  Consider accesskey '1'.  In Windows Internet Explorer, pressing Alt+1 selects the corresponding link - you must press Enter to go to that link.  In Firefox, pressing Alt+Shift+1 goes to the associated link immediately.&lt;br /&gt;&lt;br /&gt;These accesskey shortcuts take precedence over standard Window commands, so if you define an accesskey 'f' it will be acted on rather than show the File menu in Windows.&lt;br /&gt;&lt;br /&gt;In Internet Explorer, the link is not selected if the link has a style of display:none.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Search accesskey&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Most of the access keys can be defined in your DNN skin using extra HTML that is not normally seen.  However, accesskey 4 takes you straight to the Search text box.  To make this happen, I had to amend &lt;code&gt;admin/Skins/search.ascx&lt;/code&gt; to add AccessKey="4" to the txtSearchNew asp:TextBox.  &lt;br /&gt;&lt;br /&gt;While there, I took the opportunity to add a label for this box, as recommended for all fields to improve accessibility.  This label is present but not normally displayed:&lt;br /&gt;&lt;code&gt;&amp;lt;asp:Label AssociatedControlID="txtSearchNew" style="display:none;" runat="server"&amp;gt;Search:&amp;lt;/asp:Label&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Main accesskeys&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The main site access keys can be defined using code like this at the top of your skin, which I adapted from another web site:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&amp;lt;ul id="skips"&amp;gt;&lt;br /&gt;&amp;lt;li&amp;gt;&amp;lt;a href="#content" accesskey="s"&amp;gt;Skip to page content, accesskey=s&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;&amp;lt;li&amp;gt;&amp;lt;asp:HyperLink ID="SkipToHome" NavigateUrl="~/" AccessKey="1" runat="server"&amp;gt;Skip to Home page, accesskey=1&amp;lt;/asp:HyperLink&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;CSS (below) can then be used to make sure that this information is not normally seen, but each link become visible when it has the focus or is active.  The list-style is set to none to avoid bullet points etc.  When not in focus, the links have width and height 0; when in focus these are set to auto.  Tweak the other settings as you wish.&lt;br /&gt;&lt;br /&gt;Finally, don't forget to provide an anchor within your skin for the skip to navigation link:&lt;br /&gt;&amp;lt;a id="content" name="content"&amp;gt;&amp;lt;/a&amp;gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;CSS code:&lt;/strong&gt;&lt;br /&gt;&lt;code&gt;ul#skips { list-style: none; }&lt;br /&gt;#skips li { list-style: none; display: inline; }&lt;br /&gt;#skips a&lt;br /&gt;{&lt;br /&gt; color: white; width: 0; height: 0; overflow: hidden; z-index: 1000;&lt;br /&gt; position: absolute; top: 15px; left: 100px;&lt;br /&gt;}&lt;br /&gt;#skips a:active, #skips a:focus&lt;br /&gt;{&lt;br /&gt; color: red; border: 1px solid red;&lt;br /&gt; width: auto; height: auto;&lt;br /&gt; display: block; overflow: visible;&lt;br /&gt; padding: 3px;&lt;br /&gt;}&lt;/code&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1593332343396110508?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1593332343396110508/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1593332343396110508' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1593332343396110508'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1593332343396110508'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/02/adding-access-key-support-to-dotnetnuke.html' title='Adding Access key support to DotNetNuke'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3346076260305952308</id><published>2010-02-01T15:10:00.015Z</published><updated>2010-02-01T15:54:19.034Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN local install with SQL Server</title><content type='html'>&lt;div&gt;The post summarises the steps required to set up DotNetNuke 5 (DNN5) locally on Windows using the full Microsoft SQL Server using SQL Server authentication, ie not the Express version. Getting the Logins and Users right is the crucial trick that I want to remember.&lt;br /&gt;&lt;br /&gt;Click on each image to see it larger.&lt;br /&gt;&lt;ol&gt;&lt;br /&gt;&lt;li&gt;In Microsoft SQL Server Management Studio, connect to your local server. Right-click on Databases on the left. Select "New database". In the following screen, enter a database name, eg "DNN522pen" and click OK without changing anything else.&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bviYI8U5I/AAAAAAAAALI/Sv40At0fMOw/s1600-h/LocalServer1.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293374343631762" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bviYI8U5I/AAAAAAAAALI/Sv40At0fMOw/s200/LocalServer1.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Now, find the server Security and right-click on Logins and select "New Login...":&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bzDR67gqI/AAAAAAAAAMI/SfsDLTUGSdk/s1600-h/LocalServer1half.png"&gt;&lt;img style="WIDTH: 197px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433297238144811682" border="0" alt="" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bzDR67gqI/AAAAAAAAAMI/SfsDLTUGSdk/s200/LocalServer1half.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Enter a login name such as "dnn522pen", select "SQL Server Authentication" and enter the password.  Untick "Enforce password policy" if you wish.  Do not set the Default database.  Click OK.&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvitHhJfI/AAAAAAAAALQ/tk3Us7vxkH4/s1600-h/LocalServer2.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293379974800882" border="0" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvitHhJfI/AAAAAAAAALQ/tk3Us7vxkH4/s200/LocalServer2.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Open up your database tree on the left: open "dnn522pen" then Security then Users.  Right-click and select "New user...":&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvi_r6loI/AAAAAAAAALY/aV_P7_fGMM0/s1600-h/LocalServer3.png"&gt;&lt;img style="WIDTH: 138px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293384959301250" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvi_r6loI/AAAAAAAAALY/aV_P7_fGMM0/s200/LocalServer3.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;In the new user dialog, enter a User name such as "dnn522pen" and the Login name you used before eg "dnn522pen".  Tick "db_owner" twice below and click OK:&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjDJpv_I/AAAAAAAAALg/FrHIYiN2ItM/s1600-h/LocalServer4.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293385889333234" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjDJpv_I/AAAAAAAAALg/FrHIYiN2ItM/s200/LocalServer4.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Next, create a new folder on disk somewhere, eg in directory "D:\dnn522pen\".  Unzip the DotNetNuke community installation file in there &lt;code&gt;DotNetNuke_Community_05.02.02_Install.zip&lt;/code&gt;.&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Open up Internet Information Services (IIS 7) Manager.  Expand the menu on the left to open Sites and Default Web Site.  Click on "Add Application..." on the right.&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjdZQKqI/AAAAAAAAALo/hI0HeZPqQak/s1600-h/LocalServer5.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293392934087330" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjdZQKqI/AAAAAAAAALo/hI0HeZPqQak/s200/LocalServer5.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Enter a suitable alias and physical path, eg "dnn522pen" and "D:\dnn522pen\":&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2b2agJS6fI/AAAAAAAAAMQ/_3g6oKJeb3k/s1600-h/LocalServer5half.png"&gt;&lt;img style="cursor:pointer; cursor:hand;width: 200px; height: 138px;" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2b2agJS6fI/AAAAAAAAAMQ/_3g6oKJeb3k/s200/LocalServer5half.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5433300935635036658" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;In IIS Manager, select the new alias on the left, select Content View and click "Browse" to start the DNN installation:&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvwq8vsSI/AAAAAAAAALw/kKecbggrhiM/s1600-h/LocalServer6.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293619910914338" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvwq8vsSI/AAAAAAAAALw/kKecbggrhiM/s200/LocalServer6.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;After a delay you should see the first DNN install screen.  Choose custom:&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bvw5rZzgI/AAAAAAAAAL4/kNvsBcVm0co/s1600-h/LocalServer7.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 168px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293623864708610" border="0" alt="" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bvw5rZzgI/AAAAAAAAAL4/kNvsBcVm0co/s200/LocalServer7.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Test file permissions.&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Now, set up your database connection:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;First, click on "SQL Server 2005/2008 Database" and wait for the screen to refresh&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Enter "(local)" for the Server&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Enter the database name, eg "dnn522pen"&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Uncheck "Integrated Security" and wait for the screen to refresh&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Enter the User ID and password&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Keep "Run as db Owner" checked&lt;/li&gt;&lt;br /&gt;&lt;li&gt;If desired, enter a database table name qualifier, eg "DNN_"&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Click on "Test Database Connection"&lt;/li&gt;&lt;br /&gt;&lt;li&gt;If OK, click on Next&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvxHwvLmI/AAAAAAAAAMA/bge8bEmLOUk/s1600-h/LocalServer8.png"&gt;&lt;img style="WIDTH: 200px; HEIGHT: 168px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293627645177442" border="0" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvxHwvLmI/AAAAAAAAAMA/bge8bEmLOUk/s200/LocalServer8.png" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Proceed with the rest of the DNN installation as normal.&lt;br /&gt;&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3346076260305952308?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3346076260305952308/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3346076260305952308' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3346076260305952308'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3346076260305952308'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/02/dnn-local-install-with-sql-server.html' title='DNN local install with SQL Server'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bviYI8U5I/AAAAAAAAALI/Sv40At0fMOw/s72-c/LocalServer1.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3091023214302048955</id><published>2010-01-15T15:25:00.007Z</published><updated>2010-01-23T12:35:17.870Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='SQL server'/><title type='text'>Importing Identity column data into SQL Server</title><content type='html'>Updated 23/1/10:&lt;br /&gt;I am in the process of porting some (DNN module) tables from one Microsoft SQL Server database to another. For various reasons, I want the primary key identity column values to remain the same. This is on a shared SQL server, so we don't have admin access, eg to the command line and file system.&lt;br /&gt;&lt;br /&gt;The SQL Server Import and Export wizard transfers the data nicely and will preserve the identity values if you do it correctly.&lt;br /&gt;&lt;br /&gt;There are two crucial tricks:&lt;br /&gt;- set up the destination table(s) with the correct primary keys etc before the copy&lt;br /&gt;- in the wizard, click Edit Mapping and tick Enable Identity Insert.&lt;br /&gt;&lt;br /&gt;----------------------&lt;br /&gt;This is alternative technique which should no longer be needed:&lt;br /&gt;&lt;br /&gt;I first export the original data to a temporary database. This creates the tables but but does not create primary keys, set default values etc. However, the copy process does preserve the original primary key identity column values.&lt;br /&gt;&lt;br /&gt;By installing the DNN module on the destination system, the tables are created correctly there. However, a wizard import into these tables (even with "Enable identity insert" set) does not preserve the key values.&lt;br /&gt;&lt;br /&gt;The trick I found is to upload the data into a new table eg "Form2", and then use the following code to copy the data into the correct "Form" table. The trick is to use the "set identity_insert" statement.&lt;br /&gt;&lt;pre&gt;set identity_insert DNN_Form on;&lt;br /&gt;&lt;br /&gt;insert into DNN_Form ([FormID],[FormName])&lt;br /&gt;select * from DNN_Form2;&lt;br /&gt;&lt;br /&gt;set identity_insert DNN_Form off;&lt;/pre&gt;&lt;br /&gt;You must list all the required columns in the insert statement.&lt;br /&gt;Only one table can have identity_insert on at a time.&lt;br /&gt;&lt;br /&gt;Finally, delete the "Form2" table.&lt;br /&gt;&lt;br /&gt;You can check the current insert identity value before and after as follows:&lt;br /&gt;&lt;pre&gt;DBCC CHECKIDENT(DNN_Form);&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3091023214302048955?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3091023214302048955/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3091023214302048955' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3091023214302048955'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3091023214302048955'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/01/importing-identity-column-data-into-sql.html' title='Importing Identity column data into SQL Server'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-9151985717387452405</id><published>2010-01-14T20:37:00.003Z</published><updated>2010-01-14T20:46:33.852Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='multi-boot'/><category scheme='http://www.blogger.com/atom/ns#' term='Windows'/><title type='text'>Wandering drive letters in Windows</title><content type='html'>My main computer's motherboard died - long live the new computer!  Thankfully, all the data on the two existing drives were OK.  A hastily bought 3.5 inch USB drive caddy worked a treat, keeping me going on the laptop.&lt;br /&gt;&lt;br /&gt;The new computer was pre-installed with Windows 7 Home Premium on drive C with a separate partition on drive D for data.  I have an MSDN subscription and I wanted to set up various versions of Windows multi-booting on the same computer.  I have two different systems that I want to run normally, one with Visual Studio 2008 and the other with various older bits of software.  I also want to have some different versions of Windows for software testing, ideally Windows 7, Vista and XP, both in x86 and x64 versions.&lt;br /&gt;&lt;br /&gt;OK - I know I can run a virtual PC, but I want to use two of these systems regularly at top speed.  I haven't tried virtual PC yet, so I decided to stick with what I knew, which is the HyperOs multibooter, though I had to upgrade to the latest version.&lt;br /&gt;&lt;br /&gt;The first task was to repartition the disk, to give more partitions, each with a Windows installation, as well as big photo/video partition.  I found that there were already two extra partitions, a (Packard Bell?) Recovery Partition and a small System Reserved partition set up by Windows 7, ie 4 Primary partitions in total.  The W7 Disk Management tool wanted to convert my disk to Dynamic Disks to give me more partitions, but these cannot be used for booting, so a swift exit was called for.&lt;br /&gt;&lt;br /&gt;HyperOS mentioned the Acronis partitioner but this wasn't playing - not sure if it was Windows 7 or the size of the disk.  I did have a copy of gparted on CD, but this hadn't worked for me before.  Eventually I found Partition Wizard www.partitionwizard.com which has worked brilliantly.  I got a free commercial licence for this.  &lt;br /&gt;&lt;br /&gt;Using Partition Wizard, I deleted (empty) drive D and made quite a few other Logical partitions.  Partition Wizard is clever enough to know that it cannot do changes in some circumstances, and so does it on reboot.  Partition Wizard can also resize partitions, moving data if need be.  So far that's worked fine.  (Check your computer power settings don't shut your disk down at an awkward moment.)&lt;br /&gt;&lt;br /&gt;Having done that, I could now copy all my precious data onto the new hard disk from the drive caddy.&lt;br /&gt;&lt;br /&gt;Anyway, I've now done various Windows installations, some by installing from DVD from Windows, but mostly by installing from DVD at reboot, ie choosing DVD at boot up from the BIOS boot menu - and choosing Custom Setup to choose the install partition.  The problem that I have found is that the drive letters that Windows uses and sees seems to change a lot.  The original W7 system is on drive C on the "first" partition.  I have another W7 on the fifth partition which thinks of itself as being on drive M - fine.  However, most other W7 and Vista installations think of themselves as being drive C, even though they are on partition 4, 7 or 8.  When I have rebooted in one of these systems, the drive letters are assigned in a fairly random way.  The DVD drive is usually drive E but not always.&lt;br /&gt;&lt;br /&gt;In Windows Disk Management you change the drive letters - for some drives at least.  But there's limited scope for what you can change to.  Partition Wizard can do this, and is probably more successful.  However there appears to be no way of persuading a Windows on partition 4 (that thinks it is at drive C) to think of itself as being drive L for example.&lt;br /&gt;&lt;br /&gt;All these wandering drive letters might not be problem.  However my software development stuff and business data has always been carefully set up (for various reasons) to be stored on both drives C and D.  I don't tend to put data in "My Documents", "Documents", "Pictures", etc because these are (usually) stored in different locations for each version of Windows.  Anyway, I have persuaded partition 2 to be drive D in all the installations so far.  However it was a problem that partition 1 kept wandering all over the shop.  My solution was to move all my crucial data from drive C onto a bigger drive D.  Ok - fairly simple in itself, but I'm still having to work out what dependencies there are in all my scripts.&lt;br /&gt;&lt;br /&gt;Another complication in this process was that Windows XP was dying during installation (with a BSOD).  This turned out to be because the SATA drives were being accessed using AHCI.  Changing the BIOS to use the SATA setting "Native IDE" got the XP installation to work.  However I did not want to leave this setting as is, so I change it whenever I want to switch to XP.  XP also doesn't recognise many of the motherboard peripherals, eg Ethernet, so the installation is not very useful.  The option to press F6 during installation would let me install a suitable driver, but (a) I don't have the driver and (b) the system doesn't have a floppy; it does look as though new motherboard has a floppy interface, but there's no connector soldered in there!&lt;br /&gt;&lt;br /&gt;I was also able to add an IDE/PATA cable and drive to the system to connect my old drives, but the installation still did not work if I was in AHCI SATA mode.&lt;br /&gt;&lt;br /&gt;I've still many applications to configure and systems to set up, but I'm getting there.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-9151985717387452405?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/9151985717387452405/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=9151985717387452405' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9151985717387452405'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9151985717387452405'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2010/01/wandering-drive-letters-in-windows.html' title='Wandering drive letters in Windows'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-4111420789151486206</id><published>2009-09-25T11:49:00.017+01:00</published><updated>2011-09-17T09:05:02.131+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='open source'/><category scheme='http://www.blogger.com/atom/ns#' term='integration'/><category scheme='http://www.blogger.com/atom/ns#' term='vtiger'/><title type='text'>Starting to program vtiger CRM</title><content type='html'>&lt;p&gt;This article applies to vtiger 5.1.0 - some of the advice/code may not be accurate for later versions.&lt;/p&gt;&lt;br /&gt;&lt;p&gt;We've been investigating the vtiger CRM as a component in an open source set of tools that a business or organisation might need. A crucial idea is to link various tools together, so I have been looking at how to get information into and out of vtiger.&lt;/p&gt;&lt;p&gt;As a start, we're looking at an Outlook add-in to add an email address/name to the vtiger Contacts list, and how to export to an accounts package. There is already an Outlook vtiger add-in, but it seems pretty flaky and only does synchronisation; we want it to check a new email for existing matches and only add it if need be.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;About vtiger&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;vtiger is a Customer Relationship Manager (CRM). vtiger deals with Leads who might turn into Contacts, with a Contact associated with an Account. You can set up Products with stock levels (and Services as well) and then generate Quotes and Invoices which can be downloaded as PDFs or sent by email. All that sort of thing. It is important to know that vtiger is designed for use only by a business' staff - it is not intended to be used by customers, although vtiger can check incoming support emails.&lt;/p&gt;&lt;p&gt;vtiger is extensible and there is some basic documentation to get you started making your own modules. The instructions effectively describe how to create a new object type (eg a time sheet), represented as a table or two in the vtiger database. You can link your module and its new object type into vtiger, so that eg menu [Tools][Time sheet] shows a list of time sheets in the standard vtiger style, clicking on a record shows the detail, with an Edit option available.&lt;/p&gt;&lt;p&gt;For the rest of this piece, I'm looking at adding functionality to an existing module. This will hopefully help you even if you are starting a complete module. Be careful if you alter existing code - it may well be overwritten if you upgrade vtiger.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The vtiger database&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;You will almost certainly want to have access to the database that vtiger uses, partly so you can see what's in there, but also so you see any changes as they really are. For MySQL, using phpMyAdmin will probably be the available tool - make sure you can get in to your database.&lt;/p&gt;&lt;p&gt;If there aren't any already, make some test Contacts within vtiger. In phpMyAdmin have a look at these tables:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-family:courier new;"&gt;vtiger_contactdetails&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-family:courier new;"&gt;vtiger_contactscf&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-family:courier new;"&gt;vtiger_contactaddress&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="font-family:courier new;"&gt;vtiger_contactsubdetails&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each of these tables has a column named 'contactid' or similar as a primary or foreign key. Note that the "contact_no" column is what is displayed is vtiger as the "Contact Id", eg 'CON1'.&lt;/p&gt;&lt;p&gt;The table &lt;span style="font-family:courier new;"&gt;vtiger_modentity_num&lt;/span&gt; is used when generating the next "contact_no" value, for the appropriate module, so 2 might be next free Contact number to make 'CON2'. Later, we'll use vtiger &lt;span style="color:#33cc00;"&gt;CRMEntity&lt;/span&gt; class &lt;span style="color:#33cc00;"&gt;setModuleSeqNumber()&lt;/span&gt; function to get this value.&lt;/p&gt;&lt;p&gt;The 'contactid' numbers identifying the contact don't start at 0 or 1. In fact, vtiger assigns a unique id number across all objects, so object 792 might be a Contact and 793 might be an Invoice. These details numbers are stored in the &lt;span style="font-family:courier new;"&gt;vtiger_crmentity&lt;/span&gt; table - a deleted flag in there is set when items are first deleted - so they can recovered from the recycle bin. Later, we'll use the &lt;span style="color:#33cc00;"&gt;insertIntoCrmEntity()&lt;/span&gt; function to get a new object id.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;vtiger command structure&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;All vtiger accesses come through one root file &lt;span style="font-family:arial;"&gt;index.php&lt;/span&gt;. The parameters determine which module operation is carried out. For example, consider this URL&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;/index.php?action=DetailView&amp;amp;module=Invoice&amp;amp;record=800&amp;amp;parenttab=Inventory&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;It runs the code &lt;span style="font-family:arial;"&gt;DetailView.php&lt;/span&gt; in the directory &lt;span style="font-family:arial;"&gt;/modules/Invoice/&lt;/span&gt;. The code that runs in there, finds the "record" value and shows the relevant invoice. So, you can add to the "Detail View" functionality by adding code to this file.&lt;/p&gt;&lt;p&gt;You can create your own actions by making a new file in the relevant directory. You then need to provide a means of invoking that action - more later.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Smarty templates&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;vtiger uses the Smarty template system to generate its output. By and large, the vtiger code doesn't generate any HTML - instead, it loads up a template from TPL files in the &lt;span style="font-family:arial;"&gt;/Smarty/templates/&lt;/span&gt; directory. vtiger PHP code sets various values that are combined into the template to form the final output. Smarty has an extensive control language available for use in a template, so it can cope with complicated structures, eg it can iterate through arrays to generate multiple table rows. One Smarty template can invoke another, so finding where something is generated can be tricky.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Adding a tool option to an Invoice&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;If you view an Invoice there are Tools listed on the right hand side: "Export to PDF" and "Send Email with PDF". To add another option here, you have to drill down through the tpl files: &lt;span style="font-family:arial;"&gt;DetailView.php&lt;/span&gt; invokes template &lt;span style="font-family:arial;"&gt;Inventory/InventoryDetailView.tpl&lt;/span&gt; which includes template &lt;span style="font-family:arial;"&gt;Inventory/InventoryActions.tpl&lt;/span&gt;.&lt;/p&gt;&lt;p&gt;In &lt;span style="font-family:arial;"&gt;InventoryActions.tpl&lt;/span&gt;, after this line:&lt;/p&gt;&lt;pre&gt;&amp;lt;!-- To display the Export To PDF link for PO, SO, Quotes and Invoice - ends --&amp;gt;&lt;/pre&gt;&lt;p&gt;add in this code:&lt;/p&gt;&lt;pre&gt;{if $MODULE eq 'Invoice'}&lt;br /&gt;{assign var=send_accounts_action value="SendAccounts"}&lt;br /&gt;&amp;lt;tr&amp;gt;&amp;lt;td align="left" style="padding-left:10px;"&amp;gt;&lt;br /&gt;&amp;lt;a href="index.php?module={$MODULE}&amp;amp;action={$send_accounts_action}&amp;amp;return_module={$MODULE}&amp;amp;return_action=DetailView&amp;amp;record={$ID}&amp;amp;return_id={$ID}" class="webMnu"&amp;gt;&amp;lt;img src="{'actionGenerateInvoice.gif'@vtiger_imageurl:$THEME}" hspace="5" align="absmiddle" border="0"/&amp;gt;&amp;lt;/a&amp;gt;&lt;br /&gt;&amp;lt;a href="index.php?module={$MODULE}&amp;amp;action={$send_accounts_action}&amp;amp;return_module={$MODULE}&amp;amp;return_action=DetailView2&amp;amp;record={$ID}&amp;amp;return_id={$ID}" class="webMnu"&amp;gt;Send to Accounts&amp;lt;/a&amp;gt;&lt;br /&gt;&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;{/if}&lt;/pre&gt;&lt;p&gt;Upload &lt;span style="font-family:arial;"&gt;InventoryActions.tpl&lt;/span&gt; and view an Invoice - check that "Send to Accounts" is visible on the right.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Adding to accounts&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Now, create a file &lt;span style="font-family:arial;"&gt;SendAccounts.php&lt;/span&gt; - probably copying an existing file such as &lt;span style="font-family:arial;"&gt;DetailView.php&lt;/span&gt; makes sense.&lt;/p&gt;&lt;p&gt;OK - this bit is skimpy for now, but it might get you started. Near the end, add in the following code to query the vtiger database and display a little info from each record found. To get the list of products ordered you will also have to query the &lt;span style="font-family:courier new;"&gt;vtiger_inventoryproductrel&lt;/span&gt; table.&lt;/p&gt;&lt;pre&gt;$query="select * from vtiger_invoice where invoiceid=?";&lt;br /&gt;$invoice_id = $focus-&gt;id;&lt;br /&gt;$result = $adb-&gt;pquery($query, array($invoice_id));&lt;br /&gt;$noofrows = $adb-&gt;num_rows($result);&lt;br /&gt;$final_output = "";&lt;br /&gt;while ($inv_info = $adb-&gt;fetch_array($result))&lt;br /&gt;{&lt;br /&gt;$invoice_no = $inv_info['invoice_no'];&lt;br /&gt;$subject = $inv_info['subject'];&lt;br /&gt;$invoicedate = $inv_info['invoicedate'];&lt;br /&gt;$accountid = $inv_info['accountid'];&lt;br /&gt;$total = $inv_info['total'];&lt;br /&gt;$final_output .= "$invoice_no: $subject $invoicedate $accountid $total&amp;lt;br/&amp;gt;";&lt;br /&gt;}&lt;br /&gt;$smarty-&gt;assign("SEND_DETAILS", $final_output);&lt;br /&gt;$smarty-&gt;display("Inventory/SendToAccounts.tpl");&lt;/pre&gt;&lt;p&gt;At the end of our new code, the Smarty variable "&lt;span style="color:#3366ff;"&gt;SEND_DETAILS&lt;/span&gt;" is set to the string we want to display. Then the Smarty template &lt;span style="font-family:arial;"&gt;SendToAccounts.tpl&lt;/span&gt; is run.&lt;/p&gt;&lt;p&gt;You had best create the template file &lt;span style="font-family:arial;"&gt;SendToAccounts.tpl&lt;/span&gt; initially as a copy of an existing template, eg &lt;span style="font-family:arial;"&gt;InventoryDetailView.tpl&lt;/span&gt;. You will need to pare it down - and eventually add in a line like the following to display your output:&lt;/p&gt;&lt;pre&gt;&amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Storing data&amp;lt;/b&amp;gt;: {$SEND_DETAILS}&amp;lt;/p&amp;gt;&lt;/pre&gt;&lt;p&gt;OK - all we've done so far is display the invoice details. Actually talking to an accounts system is beyond the scope of this article.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Adding a new contact&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;As I said earlier, I want to have an Outlook add-in that can be used to add a contact to the vtiger database, after checking to see whether the contact already exists. As the first step to getting this done, I set up a small web form that has First name, Last name and Email fields. The Go button is set up to go to this vtiger URL eg:&lt;br /&gt;&lt;br /&gt;&lt;a&gt;&lt;span style="font-family:courier new;"&gt;/index.php?action=AddOutlookContact&amp;amp;module=Contacts&amp;amp;FirstName=Chris&amp;amp;LastName=Cant&amp;amp;Email=sales@phdcc.com&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;As per usual, vtiger uses its standard rules so that this script is called:&lt;br /&gt;&lt;span style="font-family:arial;"&gt;/modules/Contacts/AddOutlookContact.php&lt;/span&gt;&lt;/p&gt;&lt;p&gt;I made &lt;span style="font-family:arial;"&gt;AddOutlookContact.php&lt;/span&gt; in a similar way to that described above, using the title "Import from Outlook" for the main content tab. The code generates the output HTML in the PHP script - a neater final approach would put the raw HTML within the template. Anyway, the PHP code generates its HTML in &lt;span style="color:#33cc00;"&gt;$Output&lt;/span&gt; and then invokes my new template &lt;span style="font-family:arial;"&gt;AddOutlookContact.tpl&lt;/span&gt;:&lt;/p&gt;&lt;pre&gt;$smarty-&gt;assign("IMPORT_DETAILS", $Output);&lt;br /&gt;$smarty-&gt;display("AddOutlookContact.tpl");&lt;/pre&gt;&lt;p&gt;In the guts of &lt;span style="font-family:arial;"&gt;AddOutlookContact.tpl&lt;/span&gt;, the output is reproduced:&lt;/p&gt;&lt;pre&gt;&amp;lt;div&amp;gt;{$IMPORT_DETAILS}&amp;lt;/div&amp;gt;&lt;/pre&gt;&lt;p&gt;The tasks within &lt;span style="font-family:arial;"&gt;AddOutlookContact.php&lt;/span&gt; are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Get passed parameters&lt;/li&gt;&lt;li&gt;Check parameters&lt;/li&gt;&lt;li&gt;Find any matching names or emails&lt;/li&gt;&lt;li&gt;Show matches to user&lt;/li&gt;&lt;li&gt;Show Add Contact button&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So: the user can see any existing matches, and can decide whether or not to press the Add Contact button, which goes to another script &lt;span style="font-family:arial;"&gt;AddOutlookContact2.php.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;The SQL script to query the existing contacts is as follows:&lt;/p&gt;&lt;pre&gt;SELECT c.*, a.accountname, crm.deleted FROM vtiger_contactdetails AS c&lt;br /&gt;LEFT JOIN vtiger_account AS a USING (accountid)&lt;br /&gt;INNER JOIN vtiger_crmentity AS crm ON crm.crmid=c.contactid&lt;br /&gt;WHERE lastname LIKE '%$QuotedLastName%' OR email LIKE '%$QuotedEmail%'";&lt;/pre&gt;&lt;p&gt;Note that I also use the function &lt;span style="color:#33cc00;"&gt;mysql_real_escape_string()&lt;/span&gt; to ensure that the passed strings cope with single and double quotes etc.&lt;/p&gt;&lt;p&gt;The SQL links &lt;span style="font-family:courier new;"&gt;vtiger_contactdetails&lt;/span&gt; with the &lt;span style="font-family:courier new;"&gt;vtiger_crmentity&lt;/span&gt; table so the Deleted column can be picked up. The &lt;span style="font-family:courier new;"&gt;vtiger_account&lt;/span&gt; table is also included so any associated account name can be found.&lt;/p&gt;&lt;p&gt;The code goes through all the found matches and displays the results to the user. I provided a link to each contact - this is the code for the link:&lt;/p&gt;&lt;pre&gt;$MatchLink = "index.php?action=DetailView&amp;amp;module=Contacts&amp;amp;record=$Match_ContactId&amp;amp;parenttab=Support";&lt;/pre&gt;&lt;p&gt;The following form shows a suitable "Add new contact" button:&lt;/p&gt;&lt;pre&gt;&amp;lt;form action='index.php' method='post' onsubmit='VtigerJS_DialogBox.block();'&amp;gt;&lt;br /&gt;&amp;lt;input type='hidden' value='AddOutlookContact2' name='action' /&amp;gt;&lt;br /&gt;&amp;lt;input type='hidden' value='Contacts' name='module' /&amp;gt;&lt;br /&gt;&amp;lt;input type='hidden' value='$htmlLastName' name='LastName' /&amp;gt;&lt;br /&gt;&amp;lt;input type='hidden' value='$htmlFirstName' name='FirstName' /&amp;gt;&lt;br /&gt;&amp;lt;input type='hidden' value='$htmlEmail' name='Email' /&amp;gt;&lt;br /&gt;&amp;lt;input type='submit' value='Add new contact' /&amp;gt;&lt;br /&gt;&amp;lt;/form&amp;gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="font-family:arial;"&gt;AddOutlookContact2.php&lt;/span&gt; has a similar core as before, except it generates HTML that shows what it has done. Here's what I did to add a new contact. This is probably not the recommended vtiger method, but it seems to get the job done.&lt;/p&gt;&lt;pre&gt;$adb-&gt;startTransaction();&lt;br /&gt;$NewContactNo = $focus-&gt;setModuleSeqNumber("increment",'Contacts'); // updates vtiger_modentity_num&lt;br /&gt;$focus-&gt;insertIntoCrmEntity('Contacts'); // updates vtiger_crmentity and sets $focus-&gt;id&lt;br /&gt;&lt;br /&gt;$query = "insert into vtiger_contactdetails (contactid,contact_no,firstname,lastname,email) values(?,?,?,?,?)";&lt;br /&gt;$qparams = array($focus-&gt;id, $NewContactNo, $FirstName, $LastName, $Email);&lt;br /&gt;$adb-&gt;pquery($query, $qparams);&lt;br /&gt;&lt;br /&gt;$query = "insert into vtiger_contactscf (contactid) values(?)";&lt;br /&gt;$qparams = array($focus-&gt;id);&lt;br /&gt;$adb-&gt;pquery($query, $qparams);&lt;br /&gt;&lt;br /&gt;$query = "insert into vtiger_contactaddress (contactaddressid) values(?)";&lt;br /&gt;$qparams = array($focus-&gt;id);&lt;br /&gt;$adb-&gt;pquery($query, $qparams);&lt;br /&gt;&lt;br /&gt;$query = "insert into vtiger_contactsubdetails (contactsubscriptionid) values(?)";&lt;br /&gt;$qparams = array($focus-&gt;id);&lt;br /&gt;$adb-&gt;pquery($query, $qparams);&lt;br /&gt;&lt;br /&gt;$adb-&gt;completeTransaction();&lt;/pre&gt;&lt;p&gt;Here are the steps:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Call &lt;span style="color:#33cc00;"&gt;setModuleSeqNumber()&lt;/span&gt; to get the next contact no eg CON2&lt;/li&gt;&lt;li&gt;Call &lt;span style="color:#33cc00;"&gt;insertIntoCrmEntity()&lt;/span&gt; to get the next contact id, eg 794&lt;/li&gt;&lt;li&gt;Insert the basic details into &lt;span style="font-family:arial;"&gt;vtiger_contactdetails&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Insert the contact id into &lt;span style="font-family:arial;"&gt;vtiger_contactscf&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Insert basic rows into &lt;span style="font-family:arial;"&gt;vtiger_contactaddress&lt;/span&gt; and &lt;span style="font-family:arial;"&gt;vtiger_contactsubdetails&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The output provides a suitable link to the new Contact.&lt;/p&gt;&lt;p&gt;Later, I made a real Outlook 2007 addin to call this vtiger code.  An extra button is shown on the ribbon when you read an email.  When you click the button, it picks up the sender's name and email address and shows the correct URL with parameters in the user's default browser.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-4111420789151486206?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/4111420789151486206/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=4111420789151486206' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4111420789151486206'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4111420789151486206'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/09/starting-to-program-vtiger-crm.html' title='Starting to program vtiger CRM'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-6066066824192111927</id><published>2009-08-26T15:28:00.010+01:00</published><updated>2010-03-17T20:48:48.457Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN no container span causes validation error</title><content type='html'>If you turn off the DNN container for a module, then DNN by default inserts an extra SPAN that can cause a W3C validation error. One solution is to have a minimal container.&lt;br /&gt;&lt;br /&gt;I had turned off the "Display Container" option for some modules so that no title is displayed. (In Admin Edit mode, the default container is used so that you can get at settings etc.) The HTML produced by DNN looked like this, within the skin pane:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;a name="402"&amp;gt;&amp;lt;/a&amp;gt;/&lt;br /&gt;&amp;lt;span id="dnn_ctr402_ContentPane" class="DNNAlignleft"&amp;gt;&lt;br /&gt;&amp;lt;!-- Start_Module_402 --&amp;gt;&lt;br /&gt;&amp;lt;div id="dnn_ctr402_ModuleContent"&amp;gt;&lt;/span&gt;&lt;br /&gt;and then the module content.&lt;br /&gt;&lt;br /&gt;This causes a validation error because you cannot nest a DIV within a SPAN.&lt;br /&gt;&lt;br /&gt;My solution is not to use the "Display Container" option - instead, I created a container with no title and used that instead. In the replacement container, I moved the [ACTIONS] and [ICON] into the footer.  This NoTitleContainer container files are available here: &lt;a href="http://www.phdcc.com/download/DNN/NoTitleContainer.zip"&gt;http://www.phdcc.com/download/DNN/NoTitleContainer.zip&lt;/a&gt;.  Unzip in /Portals/_default/Containers/MinimalExtropy.  I'm not sure if you need to go to [Admin][Skins] to make the container visible.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-6066066824192111927?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/6066066824192111927/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=6066066824192111927' title='9 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6066066824192111927'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6066066824192111927'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/08/dnn-no-container-span-causes-validation.html' title='DNN no container span causes validation error'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>9</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1231242258420840318</id><published>2009-06-25T22:26:00.005+01:00</published><updated>2009-06-26T09:36:56.589+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN Event Viewer spider errors</title><content type='html'>If you are getting "Page Load Exception" errors in the DotNetNuke (DNN) [Admin][Event Viewer] with the UserAgent indicating a web crawling spider, then you need this fix:&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;The fix: &lt;/strong&gt;Get the &lt;a href="http://www.oliverhine.com/DotNetNuke/Downloads.aspx"&gt;Browser Caps - v2&lt;/a&gt; by Oliver Hine. Unzip the file &lt;code&gt;App_Browsers.zip&lt;/code&gt; to get the file &lt;code&gt;OtherSpiders.browser&lt;/code&gt; - put this in the &lt;code&gt;App_Browsers&lt;/code&gt; directory of your DNN site. Note that your DNN site will automatically restart, so perhaps do this at a quiet time.&lt;br /&gt;&lt;br /&gt;Alternatively (if you want), you should be able to stop these spiders accessing your site using a suitable &lt;code&gt;robots.txt&lt;/code&gt; file.&lt;br /&gt;&lt;br /&gt;This patch is needed for DNN 4.9.4 and possibly earlier. I think that it may be fixed in DNN 5.1+.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Spiders fixed: &lt;/strong&gt;This file has fixes for these spiders: Baiduspider, Yandex, ia_archiver, Sphere, Feedfetcher-Google, Yanga, worio, zibber, &lt;strike&gt;twiceler, voliabot&lt;/strike&gt;, aisearchbot, robotgenius and R6_FeedFetcher.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Spiders not fixed:&lt;/strong&gt; However it doesn't fix TweetmemeBot - I failed to add support for this. Read this &lt;a href="http://mark.kolich.com/2009/04/tweetmemebots-invalid-user-agent-string.html"&gt;Tweetmeme&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Update: doesn't seem to work for Twiceler or voilabot.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;What's happening: &lt;/strong&gt;DNN code is asking ASP.NET for details of the browser capabilities. ASP.NET gets this from the UserAgent string. DNN asks for the major and minor version numbers of the browser and causes an exception if these cannot be determined from the UserAgent. ASP.NET uses various &lt;code&gt;.browser&lt;/code&gt; files (in the framework directories and in the web app's &lt;code&gt;App_Browsers&lt;/code&gt; directory) to work out what browsers can do. As the UserAgent provided by these spiders doesn't follow a standard regex pattern, ASP.NET cannot get the major and minor versions.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Thanks to: &lt;/strong&gt;&lt;a href="http://twitter.com/BarrySweeney"&gt;Barry Sweeney&lt;/a&gt; for his prompt help on Twitter - and Oliver Hine of course.&lt;br /&gt;&lt;br /&gt;Below is the DNN exception that I get before applying this fix:&lt;br /&gt;&lt;br /&gt;&lt;hr /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Message: DotNetNuke.Services.Exceptions.PageLoadException: Value cannot be null. Parameter name: String ---&gt; System.ArgumentNullException: Value cannot be null. Parameter name: String at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer&amp;amp; number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Web.Configuration.HttpCapabilitiesBase.get_MajorVersion() at DotNetNuke.UI.Utilities.ClientAPI.BrowserSupportsFunctionality(ClientFunctionality eFunctionality) at DotNetNuke.UI.Utilities.ClientAPI.get_ROW_DELIMITER() at DotNetNuke.UI.Utilities.ClientAPI.RegisterClientVariable(Page objPage, String strVar, String strValue, Boolean blnOverwrite) at DotNetNuke.UI.Skins.Controls.Search.Page_PreRender(Object sender, EventArgs e) at System.Web.UI.Control.OnPreRender(EventArgs e) at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) --- End of inner exception stack trace ---&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1231242258420840318?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1231242258420840318/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1231242258420840318' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1231242258420840318'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1231242258420840318'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/06/dnn-event-viewer-spider-errors.html' title='DNN Event Viewer spider errors'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-9106045606662390258</id><published>2009-05-27T16:41:00.007+01:00</published><updated>2009-05-27T18:11:33.974+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><category scheme='http://www.blogger.com/atom/ns#' term='skin'/><title type='text'>DNN Skin token support in a module</title><content type='html'>This article shows how to get a DotNetNuke (DNN) module to support tokens in skins. It will use, as an example, how I added support for the [CODEMODULE] skin token in our &lt;a href="http://www.phdcc.com/phdcc.CodeModule/"&gt;phdcc.CodeModule&lt;/a&gt; retail module.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Introduction&lt;/strong&gt;: skins are used by DNN as a template for the whole site output, and a container template is used to wrap an individual module. Skins and containers can contain standard tokens such as [LOGO] - this is replaced at runtime by the logo image chosen by the site administrator.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Doing it manually&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The crucial trick is to add a suitable entry to the &lt;span style="font-family:courier new;"&gt;ModuleControls&lt;/span&gt; table.&lt;br /&gt;&lt;br /&gt;This table primarily contains a mapping between the blank (View), Edit and Settings control keys for a module and the actual controls that implement them, eg the "Settings" ControlKey is set to "DesktopModules/phdcc.CodeModule/Settings.ascx" for my module.&lt;br /&gt;&lt;br /&gt;To support your new skin token, add a new row with your skin token name in the &lt;em&gt;ControlKey&lt;/em&gt; column (eg "CodeModule" with no quotes or brackets), a NULL for the &lt;em&gt;ControlTitle&lt;/em&gt;, your view control in &lt;em&gt;ControlSrc&lt;/em&gt; and -2 in &lt;em&gt;ControlType&lt;/em&gt;. For my module, the &lt;em&gt;ControlSrc&lt;/em&gt; is "DesktopModules/phdcc.CodeModule/View.ascx"&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Adding a token to a skin&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;People work with skins in various ways. The standard method is to have an HTML template file that is parsed into an ASCX when the skin is uploaded. If you edit the HTML file on a live site then you can click on [Parse Skin Package] to re-make the ASCX.&lt;br /&gt;&lt;br /&gt;Anyway, add your skin token to your HTML skin somewhere, eg add "[CODEMODULE]", without quotes but with brackets - and in upper case. Either package it up and upload, or edit the live version and click on [Parse Skin Package]. The parse log should show your token being replaced. The ASCX should have your token replaced, as per the example below.&lt;br /&gt;&lt;br /&gt;If you work with an ASCX template file, then you need to add a suitable Register directive at the top of the ASCX, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;%@ Register TagPrefix="dnn" TagName="CODEMODULE" Src="~/DesktopModules/phdcc.CodeModule/View.ascx" %&amp;gt;&lt;/span&gt;&lt;br /&gt;then add instances of this control later on, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;dnn:codemodule runat="server" id="dnnCODEMODULE" /&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Hopefully your view control should now be called whenever this skin is used. If so, hurrah! Note that the PortalModuleBase TabModuleId is set to -1 so you could use this to tell when you are being called from a skin.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Token parameters&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;It is useful to be able to pass parameters to your module, eg so it knows what to display. The parameters are set in the skin.xml file alongside your HTML template. For my module, I wanted to add a ControlFile parameter to tell my module what to do. I added this to skin.xml, after &amp;lt;Objects&amp;gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;object&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;lt;token&amp;gt;[CODEMODULE]&amp;lt;/token&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;lt;settings&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;setting&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;name&amp;gt;ControlFile&amp;lt;/name&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;value&amp;gt;viewSkin.ascx&amp;lt;/value&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/setting&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;lt;/settings&amp;gt;&lt;br /&gt;&amp;lt;/object&amp;gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;You can specify several settings for your token.  In this case, I set the "ControlFile" setting to value "viewSkin.ascx". Make sure [CODEMODULE] is in upper case.&lt;br /&gt;&lt;br /&gt;To support each parameter, you must add a corresponding property to your control.  So, in this case, I must add a property called "ControlFile".  In VB, this could be:&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Private _ControlFile As String&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Public Property ControlFile() As String&lt;br /&gt;&amp;nbsp;&amp;nbsp;Get&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Return _ControlFile&lt;br /&gt;&amp;nbsp;&amp;nbsp;End Get&lt;br /&gt;&amp;nbsp;&amp;nbsp;Set(ByVal value As String)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;_ControlFile = value&lt;br /&gt;&amp;nbsp;&amp;nbsp;End Set&lt;br /&gt;End Property&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;If you upload or re-parse your skin, you should find that the parameter has now been set in the ASCX, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;dnn:codemodule runat="server" id="dnnCODEMODULE" ControlFile="viewSkin.ascx" /&amp;gt; &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;When your view control is now run, the ControlFile property should have been set before your Page_Load is called.  You can use this to tell when you have been called from a skin - and act accordingly.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Automatic installation&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Earlier on, we added the crucial row to the &lt;span style="font-family:courier new;"&gt;ModuleControls&lt;/span&gt; table by hand.  To set this automatically during installation, you need to add an appropriate data provider.  For example, this might be called 01.00.00.SqlDataProvider.  Add in SQL code to delete any existing ControlKey and add the correct one, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;DELETE FROM {databaseOwner}{objectQualifier}ModuleControls&lt;br /&gt;&amp;nbsp;&amp;nbsp;WHERE ControlKey='CodeModule'&lt;br /&gt;INSERT INTO {databaseOwner}{objectQualifier}ModuleControls&lt;br /&gt;&amp;nbsp;&amp;nbsp;(ControlKey,ControlTitle,ControlSrc,ControlType)&lt;br /&gt;&amp;nbsp;&amp;nbsp;VALUES ('CodeModule',NULL,'DesktopModules/phdcc.CodeModule/View.ascx',-2) &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Finally, add in an entry to your Uninstall.SqlDataProvider file to remove this entry if your module is removed:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;DELETE FROM {databaseOwner}{objectQualifier}ModuleControls&lt;br /&gt;&amp;nbsp;&amp;nbsp;WHERE ControlKey='CodeModule'&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-9106045606662390258?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/9106045606662390258/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=9106045606662390258' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9106045606662390258'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9106045606662390258'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/05/dnn-skin-token-support-in-module.html' title='DNN Skin token support in a module'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-8375722092908504633</id><published>2009-04-14T13:49:00.005+01:00</published><updated>2009-04-14T14:20:27.354+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN development and production sites</title><content type='html'>Bro John wants me to write up how he does DotNetNuke (DNN) upgrades and site copies.  As a preamble, he wrote this:&lt;br /&gt;=============&lt;br /&gt;I would think anyone with a production site would want three copies&lt;br /&gt;A - a live site&lt;br /&gt;B - a transition site&lt;br /&gt;C - a development site&lt;br /&gt;&lt;br /&gt;also&lt;br /&gt;D - a trash site&lt;br /&gt;&lt;br /&gt;As most of my stuff is now in &lt;a href="http://www.phdcc.com/phdcc.CodeModule/"&gt;phdcc.CodeModule&lt;/a&gt;s, I develop these until they work on C, I copy these to D to see if they still work in a different environ. I then copy them to A and test.&lt;br /&gt;&lt;br /&gt;I would prefer to have a B which means I can shut down A and have users running on B and then swap back to A later if for example I do a DNN version upgrade.&lt;br /&gt;&lt;br /&gt;Doing a DNN version upgrade on a live site is just asking for trouble.&lt;br /&gt;&lt;br /&gt;If this sort of stuff is not sorted in DNN properly then I wouldn't consider it a solid environment to do anything serious in.&lt;br /&gt;&lt;br /&gt;I am trying to be careful about keeping all database/email server specific stuff in a few files.  Hard links to other pages on the site are OK if you copy the entire site correctly.&lt;br /&gt;&lt;br /&gt;Maybe bigger players have other tricks they pull. Maybe they swap DNS pointers. However that takes time to permeate and produces horrid cahcing problems.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-8375722092908504633?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/8375722092908504633/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=8375722092908504633' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8375722092908504633'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/8375722092908504633'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/04/dnn-development-and-production-sites.html' title='DNN development and production sites'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1729941238407584543</id><published>2009-04-03T12:30:00.002+01:00</published><updated>2009-04-03T12:42:37.878+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='IIS'/><title type='text'>Content-Location HTTP header for current URL</title><content type='html'>All our web sites on shared host provider Crystaltech/Newtek have just started serving an extra HTTP header &lt;code&gt;Content-Location&lt;/code&gt; for all static web page requests - the header contains the current URL.&lt;br /&gt;&lt;br /&gt;To see this in action, use &lt;a href="http://www.rexswain.com/httpview.html"&gt;Rex Swain's HTTP Viewer&lt;/a&gt; to view this page at our web site: &lt;a href="http://www.phdcc.com/phd.html"&gt;http://www.phdcc.com/phd.html&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;If you look in the received output, you will see that the &lt;code&gt;Content-Location&lt;/code&gt; header is set to the requested URL:&lt;br /&gt;&lt;code&gt;Content-Location:·http://www.phdcc.com/phd.html(CR)(LF)&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Normally, the &lt;code&gt;Content-Location&lt;/code&gt; header is used to indicate when the content actually corresponds to another URL. So if you look at http://www.phdcc.com/ you will see this output:&lt;br /&gt;&lt;code&gt;Content-Location:·http://www.phdcc.com/default.htm(CR)(LF)&lt;/code&gt;&lt;br /&gt;ie the web site home page is actually called &lt;code&gt;default.htm&lt;/code&gt;.&lt;br /&gt;&lt;br /&gt;While this is a harmless change for most users, it did in fact fool our FindinSite-MS search engine - I am in the process of releasing a new version to cope with this. Our ISP Crystaltech claims that nothing has changed. Has anyone any further information?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1729941238407584543?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1729941238407584543/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1729941238407584543' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1729941238407584543'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1729941238407584543'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/04/content-location-http-header-for.html' title='Content-Location HTTP header for current URL'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5446898403530858851</id><published>2009-04-02T15:36:00.013+01:00</published><updated>2009-04-02T21:33:28.706+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='ASP.NET'/><title type='text'>Lowering ASP.NET memory usage</title><content type='html'>This blog post is a work in progress on how to keep ASP.NET web application memory usage low.  The motivation for this is to avoid the web app being stopped for using too much memory.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The Microsoft Internet Information Services (IIS) web server has administrator options to &lt;a href="http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/1652e79e-21f9-4e89-bc4b-c13f894a0cfe.mspx"&gt;Recycle Worker Threads&lt;/a&gt;, ie to automatically stop a thread that runs an ASP.NET web application if a threshold is passed.  Some of these thresholds are 'arbitrary', eg the default Elapsed Time of 29 hours.  However the Virtual Memory and Used Memory thresholds tend to kick in as you use more memory.  If a web app is 'recycled' then you get no warning - the thread is simply terminated; the web app is only restarted in response to another web request.&lt;br /&gt;&lt;br /&gt;In addition, a web app will tend to slow down as its memory use increases.&lt;br /&gt;&lt;br /&gt;I am working on this topic for my &lt;a href="http://www.phdcc.com/findinsite/"&gt;FindinSite-MS site search engine&lt;/a&gt;.  This one web app both (a) does searches and (b) crawls a web site to build a 'search database' that is used by the search.  The crawl/index task is done in a separate background thread - an indexing task is either started from the user interface, or run on a predefined schedule.  (The app has to be alive for a scheduled index to run - so an outside process can be set up to wake up a web app if need be.)&lt;br /&gt;&lt;br /&gt;This software currently suffers from two problems:&lt;br /&gt;- Too much of the database-being-searched is kept in memory&lt;br /&gt;- More significantly: The index process uses a large amount of memory.&lt;br /&gt;&lt;br /&gt;Note that my 'search database' db (as I refer to it) is just a set of files in memory - no real database is used.  This makes the search engine easier to deploy as a database is not required.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Memory heuristics&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;I use the System.GC.GetTotalMemory(false) call to get the current total memory usage, without forcing a garbage collection.&lt;br /&gt;&lt;br /&gt;I don't have a precise figure for what amount of memory is too big.  On our Crystaltech shared host, anything less than 10MB is good, while anything over 100MB is bad - though a recent indexing run worked with a maximum 400MB memory usage.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Redesign methodology&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;My initial focus was on designing a new 'search database'.  This db design needs simultaneously to be searchable and buildable, ie sufficently fast and low-memory while searching - and the same while building.&lt;br /&gt;&lt;br /&gt;The main obvious programming technique is &lt;strong&gt;not to keep anything in memory&lt;/strong&gt;.  This is actually quite a hard mindset to achieve.  For example, it would be quite nice to keep a bit of information in memory about every file indexed, eg URL, title, size, etc.  While this might work for 1,000 files, or even 25,000, it is not going to work for half a million.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Using disk instead of memory&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;If I cannot store information in memory, then I'll have to save it to disk.  In fact, this may not be as bad an option as it sounds, as the operating system (and hardware etc) will cache disk data in memory, so performance time may not go down too significantly.  Storing data on disk should not impact on ASP.NET memory usage, so should help ensure that my app isn't killed.&lt;br /&gt;&lt;br /&gt;&lt;em&gt;&lt;strong&gt;Example 1: word file list&lt;/strong&gt;&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;I want to store a list of file numbers for each word found during the crawl.  My first re-design had 32 of these numbers in memory, along with 4 other integers - with the rest on disk.  A second redesign reduced this to 8+4.  My latest design simply has 2 integers per word in memory.  Everything else is on disk.&lt;br /&gt;&lt;br /&gt;The first integer is the block number in the temporary data file.   The second number is the last inserted file number - this makes sure that I don't update the block more than once per file for each word.&lt;br /&gt;&lt;br /&gt;&lt;em&gt;&lt;strong&gt;Example 2: file list&lt;/strong&gt;&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;I want to know whether I've indexed a file before.  I used to keep 2 (yes two) lists of files in memory.  Now this is all written out to disk.&lt;br /&gt;&lt;br /&gt;So how do I check quickly if I've already indexed a file?  OK, I do have a List&amp;lt;&amp;gt; of all files indexed.  Each List element is a structure that contains two integers, a FileHash and a FilePointer.  The FileHash is the HashCode of the file path, and the FilePointer is the location of the full information on disk.&lt;br /&gt;&lt;br /&gt;To check whether I've indexed a file, I find the HashCode of the file path.  I then iterate through the List&amp;lt;&amp;gt;.  If the hash matches then I use the FilePointer to retrieve the path from disk.  If this matches, then the file has been indexed before.  I keep looking if it doesn't match, in case two or more files have the same hash.&lt;br /&gt;&lt;br /&gt;&lt;em&gt;&lt;strong&gt;Example 3: reversed word list&lt;/strong&gt;&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;To support wild cards at the start of a search, eg a search for [*hris], I need to reverse each word, so that "chris" becomes "sirhc".  [I won't reveal my algorithm just now.]&lt;br /&gt;&lt;br /&gt;At one stage, I created a list of reversed words as I went through the normal word list.  However, I now write the reversed words out to disk first.  I then clear the word list (see below) to reduce my memory footprint.  Finally I read my reversed words in for processing.&lt;br /&gt;&lt;br /&gt;&lt;em&gt;&lt;strong&gt;Example 4: SortedDictionary.Clear()&lt;/strong&gt;&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;I use a SortedDictionary during the indexing run.  If I set this to null and do a garbage collect, then no memory is freed.  If I call Clear() and then set this to null, the memory is cleared.  [And I'm pretty certain that there are no other references to items in the dictionary.]&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Should I use Cache?&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Is it safe to use the ASP.NET Page.Cache?  I presume that using this will still add to the memory used by the application, so the web app could still be shut down unilaterally, without trying to clear the Cache.&lt;br /&gt;&lt;br /&gt;I do use the Cache as a part of the search process.  However I set an expiry time of 5 minutes - this provides a useful cache while a user is searching, but clears the memory when they have probably gone anyway.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Storing data in the Session&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;I have just remembered that I store the search results for each user in a Session variable.  This is useful, eg when they ask for the second page of hits for a search.  This results data could be reasonably big, so I now think that this is not wise.  The Session variable will presumably be cleared after say 20 minutes, but this is too big a risk.  I'll have to store the results to disk, retrieve and clear as necessary.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Large Object Heap objects&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;This article on &lt;a href="http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/"&gt;The Dangers of the Large Object Heap&lt;/a&gt; says that any object larger than 85kB (or 8kB for doubles) might result in increased memory usage.  Try not to use large objects.&lt;br /&gt;&lt;em&gt;Does each collection or generic collection count as one object, or is each one a multitude of its individual components?&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Current progress&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;A crawl of a 100,000 simple HTML files now takes 11 minutes and uses a maximum of 23MB of memory.  When this db is loaded for searching, the rest state of the web app is 6MB.  After a couple of searches this went up to 22MB.&lt;br /&gt;&lt;br /&gt;The previous version took 83 minutes and used 126MB max memory.  When loaded for searching, 44MB of memory is used, going up to 50MB after a couple of searches.&lt;br /&gt;&lt;br /&gt;This is not exactly a completely fair test as the new code is not complete in several important ways.  However it does show dramatic memory usage reductions.  I am not exactly sure why there's such a dramatic speed improvement - it might be because of the reduced memory usage - or it could be the simpler not-really-complete algorithm.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5446898403530858851?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5446898403530858851/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5446898403530858851' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5446898403530858851'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5446898403530858851'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/04/lowering-aspnet-memory-usage.html' title='Lowering ASP.NET memory usage'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5858332846256294215</id><published>2009-03-29T13:40:00.007+01:00</published><updated>2009-03-31T09:25:57.173+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN portal Import/Export Link Url bug</title><content type='html'>In DNN DotNetNuke 4.92, you can use [Host][Portals] to export a complete Portal as a template to the Portals/_default/ directory.  You can then import the template elsewhere using [Admin][Site Wizard].&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Summary:&lt;/strong&gt; if you have pages that have a "Link Url" that refers to another page on the site, then these links do not survive the import/export process properly.  Currently, you will need to fix these up by hand after the import, going through [Admin][Pages] to edit the selected page's settings.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Preamble:&lt;/strong&gt; in DNN, each page is identified by an integer TabId.  This nomenclature stems from DNN's initial versions that refered to each page as a "tab".&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Details:&lt;/strong&gt; When a "Link Url" is set up to another page on the site, the page's TabId is stored as an integer in the Tabs database table Url column.  When the portal is exported, this Url TabId number is stored in the template (in a &amp;lt;url&amp;gt; XML element).  On import, the Url TabId is simply stored directly in the new page row Url column.  However, the TabId of the desired "Link Url" page will almost certainly have changed, so the page will redirect the user to the wrong page.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Background:&lt;/strong&gt; The Site Wizard does not really describe what it does very well.  An old book says that the import process is *additive*, ie the pages in the template are *added* to the existing pages on your site.  However this isn't entirely correct: if you select "Replace" in the wizard, then the existing pages are soft-deleted, ie their TabName has "_old" appended and the page is marked as Deleted.&lt;br /&gt;&lt;br /&gt;The import Site Wizard assigns a new TabId to each imported page (if a new page is created).  This means that it is virtually certain that the TabId-s have changed.  If your template etc has hard-coded links to particular TabId-s, then these are almost guaranteed to be wrong.&lt;br /&gt;&lt;br /&gt;It would be useful if it were possible to import a template and keep the original TabId-s.  Currently, the portal export does not contain the current TabId for each page.  Superficially it is also not possible to keep existing TabId-s as the Tab table TabID column is an identity.  However it is possible to use the &lt;a href="http://msdn.microsoft.com/en-us/library/aa259221(SQL.80).aspx"&gt;SET IDENTITY_INSERT&lt;/a&gt; command to insert a row with a particular TabId.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://support.dotnetnuke.com/issue/ViewIssue.aspx?id=9632&amp;PROJID=2"&gt;Reported on DNN Gemini bug tracker&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;To do:&lt;/strong&gt;&lt;br /&gt;- See if there is a better way of putting links (eg in Text/HTML and in code) so that they survive the import/export process.  &lt;em&gt;Me: in code you can do this using the TabController GetTabByName function.&lt;/em&gt;&lt;br /&gt;- Double-check what the Ignore and Merge site wizard options do&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Bro John adds:&lt;/strong&gt;&lt;br /&gt;- These problems typically cause an infinite redirect loop and that IE does not notice this but FF and Chrome do.  &lt;em&gt;Me: The problem in one case is that the Login page was not visible to anonymous users, so it redirects to the Login page ad mausoleum.  The import process does not reset the LoginTabId; it only sets it if a tab with tabtype logintab is found.  Unless the old LoginTabId is Replaced, this should be safe.&lt;/em&gt;&lt;br /&gt;- The logon menu item typically fails so you need to have added a hand made login page to be able to log in to the site once you have created it ...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5858332846256294215?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5858332846256294215/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5858332846256294215' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5858332846256294215'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5858332846256294215'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/03/dnn-portal-importexport-link-url-bug.html' title='DNN portal Import/Export Link Url bug'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-348357106747323968</id><published>2009-01-28T12:23:00.005Z</published><updated>2009-01-28T12:45:57.284Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='.NET'/><category scheme='http://www.blogger.com/atom/ns#' term='command line'/><title type='text'>Environment.GetCommandLineArgs backslash and double-quote issue</title><content type='html'>Just noticed that System.Environment.&lt;span style="font-family:arial;"&gt;GetCommandLineArgs&lt;/span&gt;() handles command line arguments that contain a backslash (\) followed by a double-quote in a strange way.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://msdn.microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx"&gt;Microsoft documentation&lt;/a&gt; says:&lt;br /&gt;"If a double quotation mark follows two or an even number of backslashes, each proceeding backslash pair is replaced with one backslash and the double quotation mark is removed. If a double quotation mark follows an odd number of backslashes, including just one, each preceding pair is replaced with one backslash and the remaining backslash is removed; however, in this case the double quotation mark is not removed."&lt;br /&gt;&lt;br /&gt;This becomes an issue when you use double-quotes to delimit parameters that may contain spaces. If a parameter is a directory such as &lt;span style="font-family:courier new;"&gt;C:\test\&lt;/span&gt; then it will be quoted as &lt;span style="font-family:courier new;"&gt;"C:\test\"&lt;/span&gt; but it will be returned by &lt;span style="font-family:arial;"&gt;GetCommandLineArgs&lt;/span&gt;() as &lt;span style="font-family:courier new;"&gt;C:\test"&lt;/span&gt; ie with a double-quote instead of a backslash.&lt;br /&gt;&lt;br /&gt;In my case, I was able to add a space between the \ and the double-quote. Then Trim() in the receiving app.&lt;br /&gt;&lt;br /&gt;The full command line is available in the Environment.CommandLine property.&lt;br /&gt;&lt;br /&gt;Also: a command line argument such as &lt;span style="font-family:courier new;"&gt;a"b"c&lt;/span&gt; is returned as &lt;span style="font-family:courier new;"&gt;abc&lt;/span&gt; by &lt;span style="font-family:arial;"&gt;GetCommandLineArgs&lt;/span&gt;().&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-348357106747323968?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/348357106747323968/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=348357106747323968' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/348357106747323968'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/348357106747323968'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/01/environmentgetcommandlineargs-backslash.html' title='Environment.GetCommandLineArgs backslash and double-quote issue'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1324625152405327156</id><published>2009-01-20T20:41:00.022Z</published><updated>2009-04-17T09:56:26.003+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='MSI'/><category scheme='http://www.blogger.com/atom/ns#' term='Custom Action'/><category scheme='http://www.blogger.com/atom/ns#' term='IIS'/><category scheme='http://www.blogger.com/atom/ns#' term='Web Setup'/><category scheme='http://www.blogger.com/atom/ns#' term='VS2008'/><category scheme='http://www.blogger.com/atom/ns#' term='Visual Studio 2008'/><category scheme='http://www.blogger.com/atom/ns#' term='msitran'/><category scheme='http://www.blogger.com/atom/ns#' term='orca'/><title type='text'>Custom VS Web Setup Projects</title><content type='html'>A Web Setup project installs an ASP.NET web application on a Microsoft Internet Information Services (IIS) web server.  The simplest web application might consist of &lt;code&gt;Default.aspx&lt;/code&gt; and &lt;code&gt;Default.aspx.cs&lt;/code&gt; files; a simple web setup project would install these files into a directory of the installer's choice and mark that directory as an IIS application, eg so that the web app runs at &lt;code&gt;http://localhost/installdir/&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;The Visual Studio 2008 Web Setup project looked like a good starting point for distributing a new version of my web application (with web service and a Forms app that tests the web service). I just needed to customise it to:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Add links to the web app in the Start Menu (eg to &lt;code&gt;http://localhost/installdir/default.aspx&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Add links to the web app on the install wizard finished page&lt;/li&gt;&lt;li&gt;Set NTFS write permissions for the web app directory&lt;/li&gt;&lt;li&gt;[Later] Make the MSI install work in Vista&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Little did I know what a task I had set myself. Perhaps buying an installation tool might have been worthwhile... Follow me, as I battle to achieve these modest aims.&lt;/p&gt;&lt;p&gt;My software is aimed at individual users and to run on servers. Running as an ASP.NET web app, it could fulfil both target systems. I want to have Start Menu links so that individuals have a means to access the web site easily. The Start Menu would also have a shortcut to a Windows Forms exe that would test the web servce.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Basic set up: Files, Program Files and Start Menu&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;First, make sure that you are targetting the right version of ASP.NET - you can choose this at the top right of the VS "Add New Project" dialog. Once your project is created, look at the project properties. Click on [Prerequisites] and make sure that your chosen base version is selected. I selected ".NET Framework 2.0 (x86)" and "Windows Installer 3.1".&lt;/p&gt;&lt;p&gt;Next, add the project output from your web app project. View the File System and add in your aspx pages (but not .aspx.cs), Global.asax and release Web.Config files, together with any other files you want in the web app directory. Make sure that the bin directory has all the needed assembly DLLs.&lt;/p&gt;&lt;p&gt;My Windows Forms application had best go in the Program Files directory, so I added a "Program Files Folder" special folder to the setup File System. I created a "Product" sub-folder for my software, and added the Forms app output to this folder. [Later on, I also put my custom installer to this folder.]&lt;/p&gt;&lt;p&gt;I added a "User's Start Menu" special folder to the setup File System. I added "Programs" then another "Product" folder inside. I want to add my URL links to the web app in here, but I had to do that in my custom installer. However I did create a shortcut to the Forms app. I had wanted to pass the actual web app URL as a parameter to this app, but this proved not to be possible, so I amended the app to get the URL from a file.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;MSI Database overview&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The web setup project creates a Windows Installer MSI file, and an accompanying Setup.exe. An MSI file has a real relational database inside that governs how the install proceeds. The database has a series of tables each with rows with various columns.&lt;/p&gt;&lt;p&gt;For example, the &lt;em&gt;Dialog&lt;/em&gt; table has one row to define each dialog that can appear during the install. The &lt;em&gt;Control&lt;/em&gt; table has rows that define each user interface element in the dialogs.&lt;/p&gt;&lt;p&gt;You can see what's in the MSI database using the &lt;strong&gt;orca&lt;/strong&gt; tool, available in the Windows SDK. Later on, I'll use orca to build a Transform that changes the main text of the finished dialog box. The &lt;strong&gt;msitran&lt;/strong&gt; tool can then be used as the setup PostBuildEvent to run this transform after every project rebuild.&lt;/p&gt;&lt;p&gt;The install process maintains various properties, some of which are defined in the &lt;em&gt;Property&lt;/em&gt; table. Other properties are set at install time - these are the ones I am interested in:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;[TARGETVDIR]: The chosen virtual directory, eg &lt;span style="font-family:courier new;"&gt;Product&lt;/span&gt;&lt;/li&gt;&lt;li&gt;[TARGETDIR]: The consequent web app path, eg &lt;span style="font-family:courier new;"&gt;C:\inetpub\wwwroot\Product&lt;/span&gt;&lt;/li&gt;&lt;li&gt;[StartMenuFolder]: The Start Menu, eg &lt;span style="font-family:courier new;"&gt;C:\Documents and Settings\All Users\Start Menu&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;NB: There are various Win32 functions that let you work with the MSI database, such as &lt;span style="font-family:arial;"&gt;MsiOpenDatabase()&lt;/span&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Custom Action Installer NET assembly&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;You can achieve some things in a .NET custom action assembly, but your code is quite isolated from the main install. I don't think you can use/edit the current MSI database, nor can you interact with any of the setup dialogs.&lt;/p&gt;&lt;p&gt;To create a custom installer, create a C# class library project - give it a useful name. Then delete the initial class1.cs. Add a new item "Installer Class" and give the file and class a meaningful name. Switch to code view. You will see that your class inherits from &lt;span style="font-family:arial;"&gt;System.Configuration.Install&lt;/span&gt;.&lt;/p&gt;&lt;p&gt;There are various overrides that you can add. You will definitely want to override &lt;span style="font-family:arial;"&gt;Install()&lt;/span&gt; and possibly &lt;span style="font-family:arial;"&gt;Commit()&lt;/span&gt;. You will almost certainly also want to override &lt;span style="font-family:arial;"&gt;Rollback()&lt;/span&gt; and &lt;span style="font-family:arial;"&gt;Uninstall()&lt;/span&gt; - to undo any changes that you have done. In fact, you must override and call &lt;span style="font-family:arial;"&gt;Install()&lt;/span&gt; even if you only want to work in the other methods.&lt;/p&gt;&lt;p&gt;For each override that you add, add a call to the base class at the start of your implementation, eg:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;public override void Install(IDictionary stateSaver)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;base.Install(stateSaver);&lt;br /&gt;}&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Compile this project and add the output to the web setup project File System Program Files "Product" sub-folder.&lt;/p&gt;&lt;p&gt;Note: if an exception is thrown in your &lt;span style="font-family:arial;"&gt;Install()&lt;/span&gt; method, the install will rollback.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Web setup Custom Actions&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In the web setup project, view the Custom Actions screen. Add a Custom Action for each of the types that you wish to support. Make sure that the InstallerClass property is True. For the Install action, set the CustomActionData as follows:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;&lt;br /&gt;/TargetDir="[TARGETDIR]\" /TargetVDir="[TARGETVDIR]" /StartMenuFolder="[StartMenuFolder]\"&lt;/span&gt;&lt;/p&gt;&lt;p&gt;This sets three parameters that are available to your custom installer assembly. Note the double quotes, and that the backslashes that seem to mandatory in two cases.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Installer custom action&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In your &lt;span style="font-family:arial;"&gt;Installer()&lt;/span&gt; code, use the base class Context.Parameters StringDictionary to retrieve the values passed in the CustomActionData:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;string StartMenuFolder = Context.Parameters["StartMenuFolder"];&lt;br /&gt;string TargetDir = Context.Parameters["TargetDir"];&lt;br /&gt;string TargetVDir = Context.Parameters["TargetVDir"];&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Getting the web app URL&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The URL of the installed web app will probably be http://localhost/product/ where product is the user's chosen virtual directory, as supplied in &lt;span style="font-family:arial;"&gt;TargetVDir&lt;/span&gt;. However I want to try to check this. This code gets the path that should correspond to http://localhost/:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;DirectoryEntry localhost1 = new DirectoryEntry(@"IIS://localhost/W3SVC/1/Root");&lt;br /&gt;string localhostRoot = localhost1.Properties["Path"][0].ToString();&lt;/span&gt;&lt;/p&gt;&lt;p&gt;If this directory is &lt;span style="font-family:courier new;"&gt;C:\inetpub\wwwroot&lt;/span&gt; and the &lt;span style="font-family:arial;"&gt;TargetDir&lt;/span&gt; is &lt;span style="font-family:courier new;"&gt;C:\inetpub\wwwroot\Product\&lt;/span&gt; then I think we can be fairly certain that appending &lt;span style="font-family:arial;"&gt;TargetVDir&lt;/span&gt; to http://localhost/ will give the correct URL of your installed web app. This code sets MakeLocalhostLinks to true if we can safely create URL shortcuts:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;bool MakeLocalhostLinks = true;&lt;br /&gt;if ((String.Compare(localhostRoot, TargetDir.Substring(0, localhostRoot.Length), true) != 0)  ||&lt;br /&gt;&amp;nbsp;&amp;nbsp;(String.Compare(@"\" + TargetVDir + @"\", TargetDir.Substring(localhostRoot.Length), true) != 0))&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;MessageBox.Show("Could not create Start Menu links", "Installer");&lt;br /&gt;&amp;nbsp;&amp;nbsp;MakeLocalhostLinks = false;&lt;br /&gt;}&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Add URL links to the Start Menu&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I can now find our Start Menu folder that has just been created and add links there. A link is a text file with extension &lt;span style="font-family:courier new;"&gt;.url&lt;/span&gt; that has one line containing &lt;span style="font-family:courier new;"&gt;[InternetShortcut]&lt;/span&gt; and the next specifying the link after &lt;span style="font-family:courier new;"&gt;URL=&lt;/span&gt;&lt;/p&gt;&lt;p&gt;The following code does this job, creating a link to &lt;span style="font-family:courier new;"&gt;begin.aspx&lt;/span&gt; on the web app site, as well as creating a fixed link to the product documentation. In both cases, the path to the newly created link is saved in the stateSaver IDictionary, for use later on rollback or uninstall.&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;string UserStartMenuFolder = StartMenuFolder + @"Programs\product\";&lt;br /&gt;if (Directory.Exists(UserStartMenuFolder))&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;if (MakeLocalhostLinks)&lt;br /&gt;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;string BeginLinkPath = UserStartMenuFolder + @"Begin.url";&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;using (StreamWriter twBeginLink = new StreamWriter(BeginLinkPath))&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;twBeginLink.WriteLine("[InternetShortcut]");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;twBeginLink.WriteLine("URL=http://localhost/" + TargetVDir + "/begin.aspx");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stateSaver.Add("BeginLinkPath", BeginLinkPath);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;string InfoLinkPath = UserStartMenuFolder + @"Product documentation.url";&lt;br /&gt;&amp;nbsp;&amp;nbsp;using (StreamWriter twInfoLink = new StreamWriter(InfoLinkPath))&lt;br /&gt;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;twInfoLink.WriteLine("[InternetShortcut]");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;twInfoLink.WriteLine("URL=http://www.example.com/product/");&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stateSaver.Add("InfoLinkPath", InfoLinkPath);&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Commit&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Commit is called when the install is complete.  You cannot make changes to the stateSaver during &lt;span style="font-family:arial;"&gt;Commit()&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:arial;"&gt;Commit()&lt;/span&gt; could be a good time to present some extra choices to the user, start an application or show a web page.  Note that the installer waits for your method to return before continuing.  You could spawn another process if you want to leave a dialog box open.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Rollback and Uninstall&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;If anything goes wrong with the install, or an uninstall occurs, you need to undo any changes you have made.  In &lt;span style="font-family:arial;"&gt;Rollback()&lt;/span&gt; and &lt;span style="font-family:arial;"&gt;Uninstall() &lt;/span&gt;you have access to the values that were saved in stateServer.  Both &lt;span style="font-family:arial;"&gt;Rollback()&lt;/span&gt; and &lt;span style="font-family:arial;"&gt;Uninstall()&lt;/span&gt; call my own new method &lt;span style="font-family:arial;"&gt;RemoveCustomAdditions()&lt;/span&gt;.  This gets the saved path strings; if they are present then the file is deleted.&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;public override void Rollback(IDictionary stateSaver)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;base.Rollback(stateSaver);&lt;br /&gt;&amp;nbsp;&amp;nbsp;RemoveCustomAdditions(stateSaver);&lt;br /&gt;}&lt;br /&gt;public override void Uninstall(IDictionary stateSaver)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;base.Uninstall(stateSaver);&lt;br /&gt;&amp;nbsp;&amp;nbsp;RemoveCustomAdditions(stateSaver);&lt;br /&gt;}&lt;br /&gt;private void RemoveCustomAdditions(IDictionary stateSaver)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;try&lt;br /&gt;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;string BeginLinkPath = stateSaver["BeginLinkPath"] as string;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!String.IsNullOrEmpty(BeginLinkPath))&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;File.Delete(BeginLinkPath);&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;string InfoLinkPath = stateSaver["InfoLinkPath"] as string;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!String.IsNullOrEmpty(InfoLinkPath))&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;File.Delete(InfoLinkPath);&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;catch (Exception ) { }&lt;br /&gt;}&lt;br /&gt;&lt;/span&gt;During rollback or uninstall it is best if you don't throw any exceptions, so catch and ignore these.&lt;/p&gt;&lt;p&gt;My web app may create data files during normal operation.  The rollback/uninstall methods could remove these files.  However, an upgrade installation will usually uninstall the software first (even if RemovePreviousVersions is false).  To keep the data files through an upgrade, these must be left in place.  My documentation tells the user to remove these data files for a complete uninstall.&lt;/p&gt;&lt;p&gt;There should be no need to undo any file permission changes as the directory should be about to disappear.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;NTFS File Permissions&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In my &lt;span style="font-family:arial;"&gt;Install()&lt;/span&gt; method, I want to let my web app write to its directory and any sub-folders.  In IIS5, aspnet_wp is the worker process that runs all ASP.NET web apps using username "ASPNET".  In IIS6 and IIS7, the w3wp worker process runs ASP.NET web app application pool using username "Network Service".  This is the code that changes the file permissions for both these user accounts for the &lt;span style="font-family:arial;"&gt;TargetDir&lt;/span&gt;:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;DirectorySecurity security = Directory.GetAccessControl(TargetDir);&lt;br /&gt;try&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;FileSystemAccessRule access = new FileSystemAccessRule("ASPNET",&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FileSystemRights.Modify,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;InheritanceFlags.ContainerInherit  InheritanceFlags.ObjectInherit,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PropagationFlags.None,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AccessControlType.Allow);&lt;br /&gt;&amp;nbsp;&amp;nbsp;security.AddAccessRule(access);&lt;br /&gt;&amp;nbsp;&amp;nbsp;Directory.SetAccessControl(TargetDir, security);&lt;br /&gt;}&lt;br /&gt;catch (Exception ) { }&lt;br /&gt;&lt;br /&gt;try&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;FileSystemAccessRule access = new FileSystemAccessRule("Network Service",&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FileSystemRights.Modify,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;InheritanceFlags.ContainerInherit  InheritanceFlags.ObjectInherit,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PropagationFlags.None,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AccessControlType.Allow);&lt;br /&gt;&amp;nbsp;&amp;nbsp;security.AddAccessRule(access);&lt;br /&gt;&amp;nbsp;&amp;nbsp;Directory.SetAccessControl(TargetDir, security);&lt;br /&gt;}&lt;br /&gt;catch (Exception ) { }&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Changing the text in the final finished wizard page&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I had originally wanted to provide a means of starting the web app on the final wizard page.  So far I have not found a way to do this (either provide links/buttons on the page, or put checkboxes with options that are actions when Finish is pressed).&lt;/p&gt;&lt;p&gt;However I did manage to update the text to include the words "Now click on the Product links in the Start Menu".&lt;/p&gt;&lt;p&gt;Using the Microsoft SDK &lt;strong&gt;orca&lt;/strong&gt; tool, I opened a first cut of  my MSI.  Create a New Transform.  In the &lt;em&gt;Control&lt;/em&gt; table, find the row for FinishedForm BodyText.  Right-click on the Text column and Copy Cell.  Paste it into Notepad and alter the text as you want.  Copy the text and Paste Cell back into the Text cell.  Select Generate Transform and save as an &lt;span style="font-family:courier new;"&gt;.MST&lt;/span&gt; file.  Note that you cannot use "\r\n" in the cell text although line breaks can be pasted.&lt;/p&gt;&lt;p&gt;Use the Microsoft SDK &lt;strong&gt;msitran&lt;/strong&gt; tool to apply the transform to an MSI, ie in the PostBuildEvent for your setup project.  I found that calling msitran directly caused the build to fail because it has a return code of 1.  I therefore had to use a batch file that called msitran and then did something innocuous to clear the return code, eg:&lt;/p&gt;&lt;p&gt;&lt;span style="font-family:courier new;font-size:78%;"&gt;msitran.exe -a ..\AlterSetupFinishedText.mst ProductSetup.msi&lt;br /&gt;dir&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Vista Installation&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;If you double-click on the MSI itself in Vista, then it will fail with an obscure message "The installed was interrupted before.."  This is because of lack of administrator privileges, even if you are logged in as an Administrator.&lt;/p&gt;&lt;p&gt;The simplest solution to this problem is to run the Setup.exe program that the web app project creates - you are prompted by Vista UAC to OK this privilege escalation.&lt;/p&gt;&lt;p&gt;The alternative is to Run as Administrator a command prompt, and then enter &lt;span style="font-family:courier new;"&gt;msiexec /i yourInstaller.msi&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;And finally, sign your assemblies and MSI&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Ensure that your project assemblies are signed before rebuilding the Web Setup project.  Sign the MSI and associated Setup.exe.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Suggestion for Microsoft&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Can you provide a means of accessing the current MSI database in .NET Installer assemblies please.&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1324625152405327156?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1324625152405327156/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1324625152405327156' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1324625152405327156'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1324625152405327156'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2009/01/custom-vs-web-setup-projects.html' title='Custom VS Web Setup Projects'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-7688334019986027773</id><published>2008-12-05T09:48:00.006Z</published><updated>2008-12-05T10:12:30.939Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='Array'/><category scheme='http://www.blogger.com/atom/ns#' term='Merge'/><category scheme='http://www.blogger.com/atom/ns#' term='Sort'/><title type='text'>Merge two sorted integer arrays</title><content type='html'>My current programming task is to merge two sorted arrays of integers into one array while removing duplicate integers.  There are no duplicates in the source arrays A and B.  The source arrays can be altered.  The integers in each array may tend to be in blocks which don't overlap, but this isn't guaranteed.  The arrays may currently have up to 100,000 integers but will often have less than this.  The output array should be truncated to 100,000 elements.  I'm using C# .NET but that's irrelevant really - it's just the algorithm that's important.&lt;br /&gt;&lt;br /&gt;I came up with several possible solutions, and am currently trying the last.  What would you do?&lt;br /&gt;&lt;br /&gt;1.&lt;br /&gt;Walk through A and B simultaneously.  If an integer in B exists in A then ignore it; if it doesn't exist then add it to the end of A.&lt;br /&gt;At the end of this process, call Array.Sort on A (ie a QuickSort).&lt;br /&gt;(Note that inserting a B element into A would surely take too long as it would involve moving all other A entries up.)&lt;br /&gt;&lt;em&gt;Quite efficient but I am concerned at speed of final sort.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;2.&lt;br /&gt;Create a new linked list with all the A entries.&lt;br /&gt;Walk through the linked list and B simultaneously; for integers in B that need inserting, alter the linked list to add a new entry in the correct place.&lt;br /&gt;At end, convert linked list back into an array.&lt;br /&gt;&lt;em&gt;Not very efficient creating a linked list and converting back from linked list.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;3.&lt;br /&gt;Walk through A and B simultaneously.  Create a list structure where each element represents a block of integers in A or B, eg use integers 0-4 from A, integers 3-15 from B, integers 5-10 from A, etc.&lt;br /&gt;At the end, create a new array from the list structure.&lt;br /&gt;&lt;em&gt;This is a more complicated version of my final simpler solution.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;4.&lt;br /&gt;Not quite sure if I thought this through properly.&lt;br /&gt;Walk through A and B simultaneously.  If the current integer in B is less than the current integer in A, then swap the two.  This sounds simple but in fact the integer swapped into B will now possibly be out of order in B, so the element will need bubbling up until it finds its correct place.&lt;br /&gt;At the end, simply add the remaining B elements to A.&lt;br /&gt;&lt;em&gt;Superficially attractive but more complicated than it looks.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;5.&lt;br /&gt;Walk through A and B simultaneously creating a new table C as you go.&lt;br /&gt;&lt;em&gt;Simple&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;Unless you can suggest otherwise...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-7688334019986027773?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/7688334019986027773/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=7688334019986027773' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7688334019986027773'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7688334019986027773'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/12/merge-two-sorted-integer-arrays.html' title='Merge two sorted integer arrays'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2636649718704515739</id><published>2008-09-03T20:39:00.016+01:00</published><updated>2009-03-03T13:05:15.708Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='custom module'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>Writing DNN custom modules</title><content type='html'>This is the last of three blog entries that make up a full introduction to DotNetNuke (DNN) from a developer's point of view. This time I cover Writing Custom Modules to add your own functionality to a DNN web site. Previously I gave an &lt;a href="http://chriscant.blogspot.com/2008/09/dnn-introduction.html"&gt;Introduction to DNN&lt;/a&gt; and showed how to &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-skins.html"&gt;Write a simple DNN Skin&lt;/a&gt;. A concise version of the complete article first appeared on 1 July 2008 in UK programming magazine &lt;a href="http://www.vsj.co.uk/articles/display.asp?id=736"&gt;VSJ&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The phdcc.CodeModule module described in this article can be downloaded &lt;a href="http://dnn.phdcc.com/Downloads/tabid/54/Default.aspx"&gt;from here&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;DNN5 has a new extension/package structure for custom modules and skins - however 'legacy' DNN3 and DNN4 module zips will install in DNN 5.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Custom DNN modules can be as complex as you want. I want to get started by analysing our freeware &lt;a href="http://www.phdcc.com/phdcc.CodeModule/"&gt;phdcc.CodeModule&lt;/a&gt; module which is available at &lt;a href="http://dnn.phdcc.com/"&gt;dnn.phdcc.com&lt;/a&gt; and contains the full source. This module makes it easier to add your own functionality without having to write a whole new module. Your code has access to the database and the ASP.NET and DNN APIs.&lt;br /&gt;&lt;br /&gt;phdcc.CodeModule simply renders your own user control. Once the module is installed, write your own code, eg in file intro.ascx. Upload the file to the &lt;span style="font-family:courier new;"&gt;DesktopModules/phdcc.CodeModule/&lt;/span&gt; directory. Add an instance of the phdcc.CodeModule module to the desired DNN page and configure it to use intro.ascx. intro.ascx can contain normal HTML, ASP.NET code and DNN calls - phdcc.CodeModule will display this whenever requested.&lt;br /&gt;&lt;br /&gt;phdcc.CodeModule's initial display:&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_hkkBsHR0r5s/SL7p6Vh5TpI/AAAAAAAAAB4/t5wvJAkXB4o/s1600-h/DNN_CodeModuleStart.jpg"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241884204726832786" style="BORDER-RIGHT: brown 1px solid; BORDER-TOP: brown 1px solid; DISPLAY: block; MARGIN: 0px auto 10px; BORDER-LEFT: brown 1px solid; CURSOR: hand; BORDER-BOTTOM: brown 1px solid; TEXT-ALIGN: center" alt="" src="http://3.bp.blogspot.com/_hkkBsHR0r5s/SL7p6Vh5TpI/AAAAAAAAAB4/t5wvJAkXB4o/s400/DNN_CodeModuleStart.jpg" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;Enter your control filename in the Settings:&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/SL7p6cWAqiI/AAAAAAAAACA/_qZM0SO8Fjw/s1600-h/DNN_CodeModuleSettings.jpg"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241884206556031522" style="BORDER-RIGHT: brown 1px solid; BORDER-TOP: brown 1px solid; DISPLAY: block; MARGIN: 0px auto 10px; BORDER-LEFT: brown 1px solid; CURSOR: hand; BORDER-BOTTOM: brown 1px solid; TEXT-ALIGN: center" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/SL7p6cWAqiI/AAAAAAAAACA/_qZM0SO8Fjw/s400/DNN_CodeModuleSettings.jpg" border="0" /&gt;&lt;/a&gt; Suppose intro.ascx contains this HTML and code to say hello to the current user:&lt;br /&gt;&lt;table style="BORDER-RIGHT: brown 1px solid; BORDER-TOP: brown 1px solid; MARGIN: 5px; BORDER-LEFT: brown 1px solid; BORDER-BOTTOM: brown 1px solid"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;lt;%@ Control Language="VB" ClassName="intro" %&amp;gt;&lt;br /&gt;&amp;lt;%@ Import Namespace="DotNetNuke.Entities.Users" %&amp;gt;&lt;br /&gt;Hello &amp;lt;% =UserController.GetCurrentUserInfo.Username%&amp;gt; &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Then this is the output, within the SimpleContainer that we made earlier:&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_hkkBsHR0r5s/SL7rdjoo0gI/AAAAAAAAACI/dwboWX4APFA/s1600-h/DNN_CodeModuleOutput.JPG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241885909320258050" style="BORDER-RIGHT: brown 1px solid; BORDER-TOP: brown 1px solid; DISPLAY: block; MARGIN: 0px auto 10px; BORDER-LEFT: brown 1px solid; CURSOR: hand; BORDER-BOTTOM: brown 1px solid; TEXT-ALIGN: center" alt="" src="http://3.bp.blogspot.com/_hkkBsHR0r5s/SL7rdjoo0gI/AAAAAAAAACI/dwboWX4APFA/s400/DNN_CodeModuleOutput.JPG" border="0" /&gt;&lt;/a&gt; As stated earlier, you can switch off the container header so it simply says "Hello host". The code simply says "Hello" if no user is logged in.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;&lt;span style="font-size:130%;"&gt;Development environment&lt;/span&gt;&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;To develop for DNN, download the latest Starter kit vsi file for Visual Web Developer Express or Visual Studio 2005. Double click on this and the Visual Studio Content Installer creates a few DNN-related templates. To create a whole new DNN site, select [File][New][Web site...], "Visual Basic" and the "DotNetNuke Web Application Framework" template. After the site has been created, an instructions page is shown. You can simply compile and run if you have SQL Server 2005 Express installed. But wait...&lt;br /&gt;&lt;br /&gt;To use the full version of SQL Server, create an empty new database, typically with a SQL Server owner login, and put the connection string in Web.Config in two places as instructed. I also set the only instance of "objectQualifier" to eg "DNN_" so that all DNN database tables etc have this prefix. I also usually increase authentication+forms-timeout from 60 (seconds) to let users stay logged in for longer, also setting slidingExpiration to "true". The last task that I might do before running is to convert from Visual Studio's built-in web server to use the local IIS - using an IIS virtual directory seems to work OK. This lets me test how the site looks in FireFox etc.&lt;br /&gt;&lt;br /&gt;I then compile and run. With all the above already set up, you can select the "Auto" install option instead of the "Typical" wizard. If installed using "Auto", your first task will be to login as host and admin to change the passwords. Then go to [Host][Host Settings] eg to set the SMTP Server, and then set up the site/portal using [Admin][Site Settings]. Go back to Home and delete all the module instances - then off you go.&lt;br /&gt;&lt;br /&gt;The created project has the 'code' half of DNN supplied as DLLs in the /bin/ directory. The web pages, controls, their code-behind and other resources are installed as web site source. All this lot can take a while to compile, which can be a bit frustrating when working on your own module - I did attempt once to convert DNN to a faster Web Project but I couldn't get it to work.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;phdcc.CodeModule&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Use [Host][Module Definitions] then "Install New Module". Browse to find phdccCodeModule_01_00_02.zip and click "Install New Module". Check the installation log for errors then press "Return". The following files will have been installed under the existing root &lt;span style="font-family:courier new;"&gt;DesktopModules&lt;/span&gt; directory.&lt;br /&gt;&lt;table style="BORDER-RIGHT: brown 1px solid; BORDER-TOP: brown 1px solid; MARGIN: 5px; BORDER-LEFT: brown 1px solid; BORDER-BOTTOM: brown 1px solid"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DesktopModules&lt;br /&gt;+ phdcc.CodeModule&lt;br /&gt;+ App_LocalResources&lt;br /&gt;+ Edit.ascx.resx&lt;br /&gt;+ Settings.ascx.resx&lt;br /&gt;+ View.ascx.resx&lt;br /&gt;Edit.ascx, Edit.ascx.vb&lt;br /&gt;Settings.ascx, Settings.ascx.vb&lt;br /&gt;View.ascx, View.ascx.vb &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The module zip also contains a manifest file &lt;span style="font-family:courier new;"&gt;phdcc.CodeModule.dnn&lt;/span&gt; that tells DNN where to install each file - in addition, it says that there are three controls: a View, Edit and Settings.&lt;br /&gt;&lt;br /&gt;The &lt;strong&gt;View&lt;/strong&gt; control displays the normal module output to users. The &lt;strong&gt;Settings&lt;/strong&gt; control is at the bottom of the module settings page - visible to admin users. The &lt;strong&gt;Edit&lt;/strong&gt; control is not used by phdcc.CodeModule yet but will typically be shown to admin users to set up the module content. You can have more user controls if need be.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;View&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;View.ascx primarily contains the following placeholder into which your user control will be inserted. The view contains additional elements which are used to give instructions and show errors.&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;asp:PlaceHolder ID="ViewControl" runat="server" /&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The View.ascx.vb codebehind class is based on &lt;span style="font-family:courier new;"&gt;DotNetNuke.Entities.Modules.PortalModuleBase&lt;/span&gt; which is in turn derived from &lt;span style="font-family:courier new;"&gt;DotNetNuke.Framework.UserControlBase&lt;/span&gt; and eventually &lt;span style="font-family:courier new;"&gt;System.Web.UI.UserControl&lt;/span&gt;. These base classes contain many crucial DNN properties and objects, ranging from the &lt;span style="color:#006600;"&gt;PortalId&lt;/span&gt; to the current &lt;span style="color:#006600;"&gt;UserId&lt;/span&gt;. The View Page_Load uses &lt;span style="font-family:courier new;"&gt;PortalModuleBase.Settings&lt;/span&gt; to retrieve your control filename before adding it to the ViewControl PlaceHolder:&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Dim ControlName = CStr(Settings("control"))&lt;br /&gt;ViewControl.Controls.Add(LoadControl(ControlName))&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;If you have not specified your control yet, the code shows an asp:label and a "Settings" asp:button. If this button is clicked then you are redirected to the module's Settings - the DNN &lt;span style="font-family:courier new;"&gt;NavigateURL&lt;/span&gt; global method is used to create the correct URL to see the Settings. In DNN parlance, each page is called a "tab" so the TabID selects the page; there may be more than one module on a page, so the correct one is selected using the ModuleId:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Response.Redirect(DotNetNuke.Common.Globals.NavigateURL(PortalSettings.ActiveTab.TabID, "Module", "moduleid", ModuleId.ToString()), False)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;phdcc.CodeModule could be improved by adding an "Edit Control" option to the module menu (to let an admin user create or edit the control in a text area edit box). The existing View implements the &lt;span style="font-family:courier new;"&gt;IActionable&lt;/span&gt; interface to do this job. The &lt;span style="font-family:courier new;"&gt;ModuleActions&lt;/span&gt; get property must return a DNN &lt;span style="font-family:courier new;"&gt;ModuleActionCollection&lt;/span&gt; with a new Action added using localised text. DNN sets up this action so that selecting it redirects to the Edit control.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Settings&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The module settings page has a common user interface at the top of the page - our content is shown lower down, as shown above. The Settings.ascx user control primarily shows a labelled text box for you to type in your control filename.&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;asp:textbox id="txtControlName" cssclass="NormalTextBox" width="200" runat="server" /&amp;gt; &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The codebehind Settings class is derived from &lt;span style="font-family:courier new;"&gt;DotNetNuke.Entities.Modules.ModuleSettingsBase&lt;/span&gt;. The LoadSettings method fills the text box with the current setting on first entry. &lt;span style="font-family:courier new;"&gt;ModuleSettingsBase.TabModuleSettings&lt;/span&gt; gets the same information that is retrieved by the view using &lt;span style="font-family:courier new;"&gt;PortalModuleBase.Settings&lt;/span&gt;.  Please see my blog on &lt;a href="http://chriscant.blogspot.com/2008/05/dnn-module-and-tab-module-settings.html"&gt;DNN module and tab-module settings&lt;/a&gt; for full details of the different settings available.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;If Not Page.IsPostBack Then&lt;br /&gt;Dim ControlName = TabModuleSettings("control")&lt;br /&gt;txtControlName.Text = ControlName&lt;br /&gt;End If&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;When the Settings page Update button is pressed, the UpdateSettings method is called to store the new control filename and refresh the DNN in-memory cache:&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Dim objModules As New DotNetNuke.Entities.Modules.ModuleController&lt;br /&gt;objModules.UpdateTabModuleSetting(TabModuleId, "control", txtControlName.Text)&lt;br /&gt;SynchronizeModule()&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Localisation&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The only remaining element of phdcc.CodeModule is the localisation files in the App_LocalResources directory. In this module, only the Settings.ascx.resx file is used. In Settings.ascx, the dnn:label control is used for the filename prompt:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;dnn:label id="lblControlName" runat="server" controlname="txtControlName" suffix=":"&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;This shows a help question mark followed by a label, with the actual text picked up from the resource file "lblControlName.Text" and "lblControlName.Help" strings. Within DNN you can edit these strings or create a new resource file for a different locale. DNN looks at the logged-in user's &lt;span style="font-family:courier new;"&gt;UserInfo.Profile.PreferredLocale&lt;/span&gt; property to determine which resource to use.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Tip 1&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;We recently wrote a simple phdcc.CodeModule user control that would not save its ViewState, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;% If Not IsPostBack Then lblGiven.Text = "init" %&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;&amp;lt;asp:Label ID="lblGiven" runat="server" /&amp;gt;&lt;/span&gt;&lt;br /&gt;If a button was pressed then lblGiven would not contain "init" on postback. The problem turned out to be that embedded code blocks &amp;lt;% %&amp;gt; are run in the PreRender phase, after the ViewState had been saved. The solution was to set the label in the Page_Load method.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Tip 2&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;In a phdcc.CodeModule control, it is useful to have access to the current DNN PortalModuleBase instance. The following code achieves this:&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load&lt;br /&gt;  Dim ViewControl As Control = Me.TemplateControl.Parent&lt;br /&gt;  Dim View As Control = ViewControl.Parent&lt;br /&gt;  If TypeOf (View) Is PortalModuleBase Then&lt;br /&gt;    Dim pmb As PortalModuleBase = CType(View, PortalModuleBase)&lt;br /&gt;    Dim ps As PortalSettings = pmb.PortalSettings&lt;br /&gt;  End If&lt;br /&gt;End Sub&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Tip 3&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Sometimes it is not clear whether to use DNN or ASP.NET functions.  If in doubt, rummage around in the DNN source to see how they do it.  For example, to determine if the user is logged in, I use ASP.NET Request.IsAuthenticated.  In another case, I wanted to get someone's password given their UserId.  After getting the Username using DNN APIs, I used the ASP.NET GetUser method to return a MembershipUser that is used to get the password.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;UserInfo ui = UserController.GetUser( PortalId, UserId, true);&lt;br /&gt;MembershipUser user = Membership.GetUser(ui.Username);&lt;br /&gt;string pwd = user.GetPassword();&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;&lt;span style="font-size:130%;"&gt;Database Modules&lt;/span&gt;&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;More substantial modules will typically want to store their own information in the database.  The following example module shows a list of Text/HTML items (in an &lt;span style="color:#000099;"&gt;asp:datalist&lt;/span&gt; data bound list).  There are admin options to Add Content items and define a template in Settings.&lt;br /&gt;&lt;br /&gt;In Visual Studio, right click on the project name and select [Add New Item...], choose "DotNetNuke Dynamic Module" for VB or C#, enter a name such as "TestMod", then press OK.  You will have to do two directory renames as per the shown instructions.  If you chose C# then you will have to add to the main Web.Config &lt;span style="font-family:courier new;"&gt;codeSubDirectories&lt;/span&gt; element.&lt;br /&gt;&lt;br /&gt;In [Host][Module Definitions] click on "Import Module Definition".  In the Manifest list, select "TestMod.dnn" and click "Import Manifest".  The TestMod module can now be put on a page but will show a ModuleLoadException until the database has been set up.&lt;br /&gt;&lt;br /&gt;The new module will have code classes created in the \App_Code\TestMod\ directory.  File TestModController.vb represents your business logic and provides access to the database from your user controls.  The Text/HTML items are each represented by a TestModInfo.vb object, each of which is stored as a row in a database table.  File DataProvider.vb describes a generic database interface, while SqlDataProvider.vb proves an actual implementation of this interface.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Controller&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The generated TestModController class has a &lt;span style="font-family:courier new;"&gt;GetTestMods&lt;/span&gt; method that returns a list of &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; objects.  It finds the current data provider and calls its &lt;span style="font-family:courier new;"&gt;GetTestMods&lt;/span&gt; method, passing the current module instance id &lt;span style="font-family:courier new;"&gt;ModuleId&lt;/span&gt;.  The data provider &lt;span style="font-family:courier new;"&gt;GetTestMods&lt;/span&gt; returns an &lt;span style="color:#006600;"&gt;IDataReader&lt;/span&gt;.  The DNN Custom Business Object utility class CBO method &lt;span style="font-family:courier new;"&gt;FillCollection&lt;/span&gt; is used to create the &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; list - it sees what fields have been returned in each &lt;span style="color:#006600;"&gt;IDataReader&lt;/span&gt; record and fills the corresponding properties in each &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; instance automatically - quite a mouthful but certainly a useful tool.  There is a similar method &lt;span style="font-family:courier new;"&gt;CBO.FillObject&lt;/span&gt; to fill just one object.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Public Function GetTestMods(ByVal ModuleId As Integer) As List(Of TestModInfo)&lt;br /&gt;Return CBO.FillCollection(Of TestModInfo)(DataProvider.Instance().GetTestMods(ModuleId))&lt;br /&gt;End Function&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;The controller also contains further methods &lt;span style="font-family:courier new;"&gt;GetTestMod&lt;/span&gt;, &lt;span style="font-family:courier new;"&gt;AddTestMod&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;UpdateTestMod&lt;/span&gt; which work with a single &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; object, using matching functionality in the data provider.&lt;br /&gt;&lt;br /&gt;The controller class also implements the DNN &lt;span style="font-family:courier new;"&gt;ISearchable&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;IPortable&lt;/span&gt; interfaces, to support searching and import/export of the module content into XML.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Database SQL&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;When a module is installed, DNN looks at the manifest version number and compares it to certain filenames in the zip.  File &lt;span style="font-family:courier new;"&gt;01.00.00.SqlDataProvider&lt;/span&gt; contains the SQL instructions to create any tables, stored procedures etc required to bring the database up to version 01.00.00.  A script &lt;span style="font-family:courier new;"&gt;01.01.03.SqlDataProvider&lt;/span&gt; would take the database to version 01.01.03.  When installing as new, then both scripts will be run.  If you are upgrading from say version 01.00.01 then only the second one is run.&lt;br /&gt;&lt;br /&gt;Because you have created a module from a template, the database has not have been created.  To set this up, you need to run the &lt;span style="font-family:courier new;"&gt;01.00.00.SqlDataProvider&lt;/span&gt; script in SQL Server Management Studio, though you may want to rename some of the tables etc first.  You should also first replace {databaseOwner} with "dbo." and {objectQualifier} with the "objectQualifier" that you used, ie nothing by default.&lt;br /&gt;&lt;br /&gt;The generated TestMod SQL script creates a table called &lt;span style="font-family:courier new;"&gt;YourCompany_TestMod&lt;/span&gt; (for &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; objects) and various stored procedures that correspond to the data provider &lt;span style="font-family:courier new;"&gt;GetTestMods&lt;/span&gt;, &lt;span style="font-family:courier new;"&gt;GetTestMod&lt;/span&gt;, &lt;span style="font-family:courier new;"&gt;AddTestMod&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;UpdateTestMod&lt;/span&gt; methods.  The &lt;span style="font-family:courier new;"&gt;YourCompany_TestMod&lt;/span&gt; table contains a ModuleId field that corresponds to the unique module identifier with the same name in the DNN Modules table.&lt;br /&gt;&lt;br /&gt;When you add a TestMod module instance to a page there is no event raised to let you populate the database.  The View code knows its ModuleId, so it calls &lt;span style="font-family:courier new;"&gt;GetTestMods&lt;/span&gt; to obtain all &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; objects for the current module instance.  If it finds that there are no such objects, then it creates a default &lt;span style="font-family:courier new;"&gt;TestModInfo&lt;/span&gt; and adds it to the database.&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Dim objTestMods As New TestModController&lt;br /&gt;Dim colTestMods As List(Of TestModInfo)&lt;br /&gt;colTestMods = objTestMods.GetTestMods(ModuleId)&lt;br /&gt;If colTestMods.Count = 0 Then&lt;br /&gt;  Dim objTestMod As TestModInfo = New TestModInfo&lt;br /&gt;  objTestMods.AddTestMod(objTestMod)&lt;br /&gt;  colTestMods = objTestMods.GetTestMods(ModuleId)&lt;br /&gt;End If&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;There is no event raised when a module is deleted from a page.  So when is your per-module-instance data deleted?  DNN has a Recycle Bin for pages and modules that have been deleted.  When a module is finally deleted from this bin, the module instance row in the Modules tables is deleted - our &lt;span style="font-family:courier new;"&gt;YourCompany_TestMod&lt;/span&gt; table has a cascade delete relation on this row, thereby deleting the right rows in our table.  Unfortunately, there is no programmer event that corresponds to this final delete.&lt;br /&gt;&lt;br /&gt;Finally, the generated module also contains another SQL script file &lt;span style="font-family:courier new;"&gt;Uninstall.SqlDataProvider&lt;/span&gt; that is run if the module is uninstalled from the system in [Host][Module Definitions..].  The SQL in there naturally deletes all tables etc that were created in the database.&lt;br /&gt;&lt;br /&gt;I will leave an exploration of the rest of the code as an exercise for you dear reader.  There are no imposed limitations on how you use the database, so you can create whatever tables you want.  Your code has access to all DNN and ASP.NET data, not to mention bog standard Session variables etc, so the world is your oyster.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;And more...&lt;/strong&gt;&lt;br /&gt;If you develop a module and want to keep your source private then follow the documentation instructions.  Another option is to make the App_Code section of your module into a separate DLL class library project (while keeping the web site controls with full source).  Include the project DLL in your module's ZIP so that it installs on other systems in the /bin/ directory.&lt;br /&gt;&lt;br /&gt;Module code also has full access to the web site file system.  It is common to store files relating to a site in the relevant portal directory, eg /Portals/0/.  It would be sensible if each module worked within a suitably named sub-directory.&lt;br /&gt;&lt;br /&gt;There is stack of DNN functionality and namespaces which I haven't touched upon.  However I hope that I have given you a flavour of what DNN is and how to extend it with your code – in a simple way using phdcc.CodeModule, or by writing a full-blown custom module.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2636649718704515739?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2636649718704515739/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2636649718704515739' title='4 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2636649718704515739'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2636649718704515739'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/09/writing-dnn-custom-modules.html' title='Writing DNN custom modules'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_hkkBsHR0r5s/SL7p6Vh5TpI/AAAAAAAAAB4/t5wvJAkXB4o/s72-c/DNN_CodeModuleStart.jpg' height='72' width='72'/><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5420839711020067842</id><published>2008-09-03T20:03:00.008+01:00</published><updated>2009-03-03T13:10:20.378Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><category scheme='http://www.blogger.com/atom/ns#' term='skin'/><title type='text'>Writing DNN Skins</title><content type='html'>&lt;p&gt;This is the second of three blog entries that make up a full introduction to DotNetNuke (DNN) from a developer's point of view. This time I cover writing a skin and a container to change the appearance of a DNN web site. Last time, I gave an &lt;a href="http://chriscant.blogspot.com/2008/09/dnn-introduction.html"&gt;Introduction to DNN&lt;/a&gt; - next will be &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-custom-modules.html"&gt;Writing Custom Modules&lt;/a&gt;. A concise version of the complete article first appeared on 1 July 2008 in UK programming magazine &lt;a href="http://www.vsj.co.uk/articles/display.asp?id=736"&gt;VSJ&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The simple skin and container used in this blog can be downloaded &lt;a href="http://www.phdcc.com/download/DNN/simple.zip"&gt;here&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;DNN5 has a new extension/package structure for custom modules and skins - however 'legacy' DNN3 and DNN4 skins will install in DNN 5 - select Skin as the legacy skin type.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;This time, I look at how to create a skin that defines the appearance of a site and write custom modules that provide content on a page. The figure below shows a typical DNN layout: the skin defines the page layout, including one or more panes. An admin user can place one or more modules in a pane, with each module wrapped in a container.&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s1600-h/DNN_PageLayout.PNG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241332283448273250" style="margin: 0px auto 10px; display: block; text-align: center;" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s320/DNN_PageLayout.PNG" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;strong&gt;Skins&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;A skin package defines how DNN presents its content, ie the overall HTML of the page, all the CSS styles, and various parameters for the breadcrumb and menu screen elements. A container package defines how each module container looks in a similar way. If you install a ‘skin’ it typically contains both skin and container packages. An individual skin package may contain several different layouts, eg fixed width and full-screen instances.&lt;br /&gt;&lt;br /&gt;DNN seems to have a preference for fixed-width skins and fixed size fonts. I presume that this is an attempt to achieve a perfect appearance on the page. However, it is best to make use of all the screen width and make the page work well as font sizes are changed by the user. Fixed sizes may achieve the desired effect in Internet Explorer, as long as the user doesn't Zoom. However in FireFox, changing the text size also changes fixed point sizes in the same way as IE Zoom. For more details, see my previous blog entries: &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font.html"&gt;DotNetNuke Internet Explorer fixed size fonts&lt;/a&gt; and &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font_28.html"&gt;DotNetNuke Internet Explorer font scaling&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;These CSS files are used by DNN, in this order, with the Portal CSS included last so its definitions take precedence over any earlier ones. &lt;/p&gt;&lt;ol&gt;&lt;li&gt;Portal default CSS (in &lt;span style="font-family:courier new;"&gt;/portals/_default/default.css&lt;/span&gt;)&lt;/li&gt;&lt;li&gt;Current skin CSS&lt;/li&gt;&lt;li&gt;Current container CSS&lt;/li&gt;&lt;li&gt;Portal CSS (eg in &lt;span style="font-family:courier new;"&gt;/Portals/0/portal.css&lt;/span&gt;)&lt;/li&gt;&lt;/ol&gt;The ‘portal default CSS’ defines styles for most generic HTML elements (H1, TD etc) though strangely not P – using fixed-point font sizes in most cases; it also defines CSS classes for all the standard DNN elements.&lt;br /&gt;&lt;br /&gt;The ‘portal CSS file’ contains blank elements to let you know what the standard definitions are – this file can be updated in the [Admin][Site Settings] menu.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Skin and container development&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;If you have installed the DNN Starter Kit into Visual Studio, you can create a skin and container using [File][New][File] - "DotNetNuke Skin" template. However, to illustrate a basic skin and container, I have developed a simple example which you can download. These are the files that I used to create the example:&lt;br /&gt;&lt;table style="border: 1px solid brown; margin: 5px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;skins/simpleskin.htm&lt;br /&gt;skins/skin.css&lt;br /&gt;skins/skin.xml&lt;br /&gt;skins.zip&lt;br /&gt;&lt;br /&gt;containers/simplecontainer.htm&lt;br /&gt;containers/container.css&lt;br /&gt;containers/container.xml&lt;br /&gt;containers.zip&lt;br /&gt;&lt;br /&gt;simple.zip&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;p&gt;The files in the &lt;span style="font-family:courier new;"&gt;skins&lt;/span&gt; directory are zipped up to make skins.zip, ditto for &lt;span style="font-family:courier new;"&gt;containers&lt;/span&gt;. Then these two are zipped together to make simple.zip that contains both the skin and container packages.&lt;br /&gt;&lt;br /&gt;Each site has one current skin and one current container. When you install a skin, the files are unzipped and a skin user control is created - when chosen as the current skin, this user control is inserted into the main page output. A skin has pane elements - DNN inserts the right modules into each pane, each wrapped in a container. The current container is also a user control with a &lt;span style="color: rgb(0, 102, 0);"&gt;ContentPane&lt;/span&gt; that contains the actual module output.&lt;br /&gt;&lt;br /&gt;If you install a skin from the [Admin] menu, then the packages will be installed in directory &lt;span style="font-family:courier new;"&gt;\portals\0\skins\simple\&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;\portals\0\containers\simple\&lt;/span&gt;. The file simpleskin.htm contains various template tokens, such as [LOGO], that are expanded on installation into file simpleskin.ascx – the ASP.NET user control that is included in the DNN output. If you update the in-situ simpleskin.htm, then click on "Parse Skin Package" in [Admin][Skins] to recreate simpleskin.ascx to see your changes. (Don't include any ascx files in your final skin/container zip package.)&lt;br /&gt;&lt;br /&gt;The SimpleSkin.htm skin template defines the basic HTML of the main DNN output, typically using tables for layout. Various tokens appear in square brackets, replaced by DNN at install/runtime. The main output panes are defined in this example as table cells with the predefined correct id names. If you are better at XHTML than I am, change this template to avoid tables.&lt;/p&gt;&lt;table style="border: 1px solid brown; margin: 5px; font-family: courier new;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;lt;table class="ss_header" width="100%"&amp;gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt; &amp;lt;td&amp;gt;[LOGO]&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&amp;lt;/table&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;table class="ss_menurow" width="100%"&amp;gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt; &amp;lt;td class="ss_menu"&amp;gt;[SOLPARTMENU]&amp;lt;/td&amp;gt;&lt;br /&gt; &amp;lt;td class="ss_menu_user"&amp;gt;[USER] [LOGIN]&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&amp;lt;/table&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;table class="ss_main" width="100%"&amp;gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt; &amp;lt;td class="ss_crumb"&amp;gt;[BREADCRUMB]&amp;lt;/td&amp;gt;&lt;br /&gt; &amp;lt;td class="ss_date"&amp;gt;[CURRENTDATE]&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&amp;lt;/table&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;table class="ss_content" width="100%"&amp;gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt; &amp;lt;td id="LeftPane" runat="server" /&amp;gt;&lt;br /&gt; &amp;lt;td id="ContentPane" runat="server" /&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&amp;lt;/table&amp;gt;&lt;br /&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The skin.css file contains all the definitions you want (not forgetting to override the DNN defaults if required), eg simply:&lt;br /&gt;&lt;table style="border: 1px solid brown; margin: 5px; font-family: courier new;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;.ss_date,.ss_menu_user&lt;br /&gt;{&lt;br /&gt; text-align:right;&lt;br /&gt;}&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;The skin.xml file defines parameters that are used by DNN.  The first one shown tells DNN to use the » character as a separator between the breadcrumb page names.  The second set of parameters makes the menu horizontal, etc.&lt;br /&gt;&lt;table style="border: 1px solid brown; margin: 5px; font-family: courier new;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;lt;Objects&amp;gt;&lt;br /&gt; &amp;lt;Object&amp;gt;&lt;br /&gt;   &amp;lt;Token&amp;gt;[BREADCRUMB]&amp;lt;/Token&amp;gt;&lt;br /&gt;   &amp;lt;Settings&amp;gt;&lt;br /&gt;     &amp;lt;Setting&amp;gt;&lt;br /&gt;     &amp;lt;Name&amp;gt;Separator&amp;lt;/Name&amp;gt;&lt;br /&gt;     &amp;lt;Value&amp;gt;&amp;lt;![CDATA[ » ]]&amp;gt;&amp;lt;/Value&amp;gt;&lt;br /&gt;     &amp;lt;/Setting&amp;gt;&lt;br /&gt;   &amp;lt;/Settings&amp;gt;&lt;br /&gt; &amp;lt;/Object&amp;gt;&lt;br /&gt; &amp;lt;Object&amp;gt;&lt;br /&gt; &amp;lt;Token&amp;gt;[SOLPARTMENU]&amp;lt;/Token&amp;gt;&lt;br /&gt;   &amp;lt;Settings&amp;gt;&lt;br /&gt;     &amp;lt;Setting&amp;gt;&lt;br /&gt;     &amp;lt;Name&amp;gt;display&amp;lt;/Name&amp;gt;&lt;br /&gt;     &amp;lt;Value&amp;gt;horizontal&amp;lt;/Value&amp;gt;&lt;br /&gt;     &amp;lt;/Setting&amp;gt;&lt;br /&gt;...&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;The SkinContainer.htm container template looks similar to the skin template, with tokens replaced when the container is rendered.  The main module output is in the ContentPane.  The container.css file contains definitions for the CSS classes used in the container template.  Similarly, container.xml defines various container-level parameters.&lt;br /&gt;&lt;table style="border: 1px solid brown; margin: 5px; font-family: courier new;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&amp;lt;table width="100%" class="c_all"&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt;&amp;lt;td class="c_header" colspan="2"&amp;gt;&lt;br /&gt; &amp;lt;table width="100%"&amp;gt;&lt;br /&gt; &amp;lt;tr&amp;gt;&lt;br /&gt; &amp;lt;td&amp;gt;[SOLPARTACTIONS]&amp;lt;/td&amp;gt;&lt;br /&gt; &amp;lt;td&amp;gt;[ICON]&amp;lt;/td&amp;gt;&lt;br /&gt; &amp;lt;td style="width:100%;" class="c_header_title"&amp;gt;[TITLE]&amp;lt;/td&amp;gt;&lt;br /&gt; &amp;lt;/tr&amp;gt;&lt;br /&gt; &amp;lt;/table&amp;gt;&lt;br /&gt;&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt;&amp;lt;td class="c_content" id="ContentPane" runat="server" colspan="2" /&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;tr&amp;gt;&lt;br /&gt;&amp;lt;td&amp;gt;[ACTIONBUTTON:1]&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;td&amp;gt;[ACTIONBUTTON:2]&amp;lt;/td&amp;gt;&lt;br /&gt;&amp;lt;/tr&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;/table&amp;gt;&lt;br /&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;There are many more tokens and options that you can use.  Ideally you should also include a preview of your skin and container by including in each zip a JPG with same name as your template file.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5420839711020067842?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5420839711020067842/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5420839711020067842' title='16 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5420839711020067842'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5420839711020067842'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/09/writing-dnn-skins.html' title='Writing DNN Skins'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s72-c/DNN_PageLayout.PNG' height='72' width='72'/><thr:total>16</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-6091447768513337277</id><published>2008-09-02T08:46:00.013+01:00</published><updated>2009-02-20T19:24:50.399Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='CMS'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DNN Introduction</title><content type='html'>This is the first of three blog entries that make up a full introduction to DotNetNuke (DNN) from a developer's point of view, with the following pieces covering &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-skins.html"&gt;Skins&lt;/a&gt; and then &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-custom-modules.html"&gt;Writing Custom Modules&lt;/a&gt;. A concise version of the complete article first appeared on 1 July 2008 in UK programming magazine &lt;a href="http://www.vsj.co.uk/articles/display.asp?id=736"&gt;VSJ&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:130%;"&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;DotNetNuke is not a good name for software, but don't let that put you off. Call it DNN, and think of it as a programmable content management system that's open source and runs on ASP.NET 2+ servers. Ie it lets you create and maintain a web site easily.&lt;br /&gt;&lt;br /&gt;Although you may be familiar with the basic idea of a CMS, this first article will introduce you to DNN features, terminology and usage before getting stuck into new skins and programmable modules next time.&lt;br /&gt;&lt;br /&gt;If you want to use Microsoft technology, DNN lets you deliver a working site with relative ease - it can be handed over to a non-technical site administrator who can do all the regular site updating, eg adding pages, editing text, making announcements etc. Web space at our preferred host &lt;a href="http://www.crystaltech.com/default.aspx?ref=phdcc.com"&gt;Crystaltech&lt;/a&gt; starts at US$35 per year for a small site. At that price everyone can have a web site: I set up a badminton club site for a friend and he's now running it with no intervention from me.&lt;br /&gt;&lt;br /&gt;There are some problems running DNN in Windows Server 2008 in Medium Trust.&lt;br /&gt;&lt;br /&gt;When trying to decide between various systems, you will want to know: &lt;ul&gt;&lt;li&gt;Is it reliable? We are now running a number of DNN sites with many thousands of users and they seem solid.&lt;/li&gt;&lt;li&gt;Is it maintained? There are regular fixes and improvements for DNN, eg it now uses AJAX in places.&lt;/li&gt;&lt;li&gt;Does it have the features you want? DNN handles users well, has a good framework to add functionality, and there are lots of add-on modules available.&lt;/li&gt;&lt;li&gt;Can I learn it easily? It will always take a while to get in-depth understanding of a new system. With DNN you can set up a basic site on day one, but it might take months to get fully to grips with writing modules. DNN was a bit easier for us as we are used to ASP, ASP.NET and Microsoft hosting. To handle development, staging, production and backup servers, make sure you are able to copy all content and users to another site.&lt;/li&gt;&lt;/ul&gt;DNN is not perfect. The Text/HTML Editor is not good enough for serious use. Cute Editor and other plug-in editors may be better. They all suffer from not being clever enough at handling complex reformatting - what you see is seldom what you get. Note that users must have cookies enabled to be able to register and login at DNN.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Extensibility&lt;/strong&gt;&lt;br /&gt;&lt;div style="border: 1px solid brown; margin: 5px; float: right;"&gt;&lt;br /&gt;&lt;strong&gt;Supplied Modules&lt;/strong&gt;&lt;br /&gt;Account login&lt;br /&gt;Banner adverts&lt;br /&gt;Feed explorer&lt;br /&gt;Google Adsense&lt;br /&gt;Links&lt;br /&gt;Search Input&lt;br /&gt;Search Results&lt;br /&gt;Text/HTML&lt;br /&gt;User Account&lt;br /&gt;Announcements&lt;br /&gt;Blog&lt;br /&gt;Documents&lt;br /&gt;Events&lt;br /&gt;FAQs&lt;br /&gt;Feedback&lt;br /&gt;Forum&lt;br /&gt;Help&lt;br /&gt;IFrame&lt;br /&gt;Media&lt;br /&gt;NewsFeeds&lt;br /&gt;Reports&lt;br /&gt;Repository&lt;br /&gt;Store&lt;br /&gt;Survey&lt;br /&gt;UserDefinedTable&lt;br /&gt;UsersOnline&lt;br /&gt;Wiki&lt;br /&gt;XML&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;DNN serves most content using modules, eg a text module comes as standard, along with the others listed below. There's a thriving third-party market for modules, some for free but largely at low cost - usually listed and bought at SnowCovered.com. At phdcc, it is crucial that we can develop custom modules to provide the unique functionality that some web sites need. We have also released some freeware and commercial modules. Custom modules are written as a set of standard ASP.NET user controls, using extra DNN APIs as needed, with the end result bundled into a ZIP for distribution. Custom modules are usually written in Visual Basic like DNN itself, but C# is possible.&lt;br /&gt;&lt;br /&gt;If you like to hedge your bets, code in a DNN module could be written to run on a plain ASP.NET system. Or perhaps do it the other way round: for example, the YetAnotherForum.net open source software will run as a standalone web app, but they have added some code and packaged it as a DNN module.&lt;br /&gt;&lt;br /&gt;DNN's presentation is determined by skins which define the appearance of the whole site and each module container. Various skins are provided for free, while others are available for purchase at SnowCovered - you can write your own relatively easily. DNN skins tend to use fixed point size CSS definitions so it doesn't scale in Internet Explorer as you change text size - more on this next time and in these previous blog entries: &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font.html"&gt;DotNetNuke Internet Explorer fixed size fonts&lt;/a&gt; and &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font_28.html"&gt;DotNetNuke Internet Explorer font scaling&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Technology&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;It is worth saying right now that DNN is almost entirely database and ASP.NET driven. All DNN output is generated from a single ASP.NET page that dynamically loads whatever content needs to be shown, using many levels of ASP.NET .ascx user controls. So the site will not be as fast as from static content, but this is an acceptable price.&lt;br /&gt;&lt;br /&gt;Although all DNN requests are run through the root &lt;span style="font-family:courier new;"&gt;default.aspx&lt;/span&gt;, it has a friendly-URL HttpModule that shows a unique URL with pertinent text for each page. Note that the standard menu system is implemented in JavaScript so it does not provide hard links. So make sure that you provide hard links somewhere so that search engines can trundle through your site.&lt;br /&gt;&lt;br /&gt;The database can be in SQL Server Express or the full SQL Server. Strictly speaking, the code has been written in a generic way using a provider pattern that allows other database types to be used, but in practice I have only ever seen the Microsoft databases supported.&lt;br /&gt;&lt;br /&gt;Doing backups in DNN can be complicated. There are facilities to export and import your site as a template in XML - but I am not confident that this gets a whole site design – and does not store the users. Doing a database backup will probably get most data. However to be certain, you will also need to backup the entire file system as some modules store their data in files.&lt;br /&gt;&lt;br /&gt;DNN uses standard ASP.NET forms authentication to handle its users (recently, Windows LiveID as well). After a fresh install, the 'host' account lets you configure the entire site, while the 'admin' account has a reduced set of permissions that is suitable to give to a non-technical site administrator. One "host" DNN instance can have one or more "portals" (one for each site/domain name), so there is one admin account for each portal. We wouldn't recommend having more than one portal - it's not worth the fuss when web space is so cheap. Things go wrong if you attempt to run DNN from a sub-directory, so always make sure that it runs at the root of a domain name - one approach is to set up a sub-domain such as &lt;a href="http://dnn.phdcc.com/"&gt;dnn.phdcc.com&lt;/a&gt;. On our shared host, we did this by adding an extra DNS Host Record (A) for "dnn" that points to the IP address of a separate hosted web space.&lt;br /&gt;&lt;br /&gt;DNN has built-in support for internationalisation, so you can download language packs for DNN itself so that all DNN management will be done in your preferred language, including email templates. Well written modules will use the same techniques but will usually only have English text provided. As I understand it, there is no direct support for showing a particular module (eg French intro text) if the user's browser setting or preferred locale is French.&lt;br /&gt;&lt;br /&gt;It is helpful to understand a little of the DNN directory structure. As well as &lt;span style="font-family:courier new;"&gt;/bin/&lt;/span&gt; and &lt;span style="font-family:courier new;"&gt;/App_Code/&lt;/span&gt; directories, each supported site has a directory, so the first site portal (with a programmer &lt;span style="color: rgb(0, 102, 0);"&gt;PortalID&lt;/span&gt; of zero) has its file store at &lt;span style="font-family:courier new;"&gt;/Portals/0/&lt;/span&gt;. [Admin][File Manager] opens at this directory. If, with your super powers, you FTP a new file into this directory, then some of the DNN code may not initially see it. There is an option in File Manager to synchronize with the actual files present - don't forget to tick the Recursive box to sync all the sub-directories. You can also synchronise programmatically using the DNN &lt;span style="color: rgb(0, 102, 0);"&gt;FileSystemUtils.Synchronize&lt;/span&gt; method. Finally, the directory &lt;span style="font-family:courier new;"&gt;/Portals/_default/&lt;/span&gt; contains definitions that are available for all portal sites, eg skins and containers.&lt;br /&gt;&lt;br /&gt;DNN has a basic search built in, using the search information provided by each module. However the search will not index file documents such as PDFs.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Downloads&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;You can get the latest version of DNN, now 4.8.4, from &lt;a href="http://www.dotnetnuke.com/"&gt;http://www.dotnetnuke.com/&lt;/a&gt; once you have registered. The "Install" package is for production use. Get the "Starter Kit" and "Documentation" packages if you are doing module development - there is a full "Source" release as well. For installation, your primary requirement is a connection string for a database, and you will need to make sure that DNN ASP.NET has full read, write and delete access to the file system. An installation wizard will take you through these options and set up the host and admin accounts. There's also an "Upgrade" package - but make sure that you have done a full backup before using this. When upgrading, you must carefully merge the existing DNN &lt;span style="font-family:courier new;"&gt;Web.Config&lt;/span&gt; file with the new one supplied in &lt;span style="font-family:courier new;"&gt;Release.Config&lt;/span&gt;, in particular keeping the existing ‘machineKey’ element, ‘SiteSqlServer’ connection strings and ‘objectQualifier’ value.&lt;br /&gt;&lt;br /&gt;I have one of the standard books, 'Professional DotNetNuke ASP.NET Portals' for old DNN 3. This aims to cover setting up and administering a site, as well as writing skins and modules. I didn't find it very helpful and it is hopeless as a programmer reference as the index does not list the things I looked for. The "Documentation" download has various useful documents, but I seem to have missed a trick somewhere as I haven't found or generated a full DNN API help file. That said, it is great having the full DNN source available so you can look up methods and see how they work.&lt;br /&gt;&lt;br /&gt;I haven't ever wanted to alter the main DNN code which is just as well if you want to track upgrades. There's a large developer community at work in the DNN help forums. I made one paltry contribution to sorting out one bug. I have however updated the wiki module to send me an email whenever a wiki entry is updated; I was pleased to be able to compile the wiki project, understand the code, and make my changes with relative ease.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-size:130%;"&gt;&lt;strong&gt;Using DNN&lt;/strong&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Here's a simple overview of how to use DNN, without endless screen shots. A DNN page is typically laid out as shown below. The current skin defines various panes on the screen where you can place one or more modules. After you have added a module, it appears in a container, a rectangle with a title bar in a style determined by the container skin. DNN is generally very configurable, eg you can switch off the title bar for each container.&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s1600-h/DNN_PageLayout.PNG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241332283448273250" style="margin: 0px auto 10px; display: block; text-align: center;" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s320/DNN_PageLayout.PNG" border="0" /&gt;&lt;/a&gt; When logged in as 'host' or 'admin' you get a Control Panel at the top of the page as shown below - most functions are pretty obvious.&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/SLz0y8A8yPI/AAAAAAAAABY/cYw06KobUew/s1600-h/DNN_Control_Panel.JPG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241333222293162226" style="margin: 0px auto 10px; display: block; text-align: center;" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/SLz0y8A8yPI/AAAAAAAAABY/cYw06KobUew/s400/DNN_Control_Panel.JPG" border="0" /&gt;&lt;/a&gt; DNN supports various roles that determine which users can do what. Roles can be assigned to the view and edit visibility permissions for each page and module; the File Manager can set up view and write permissions for portal directories. Users who have the "Administrators" role can access everything. The other standard roles are 'All Users', 'Registered Users', 'Subscribers' and 'Unauthenticated Users'. You can set up new roles, for example, a fee-paying role that might be used to allow access to privileged content for say 3 months. DNN has built in support for several payment providers, including PayPal. &lt;p&gt;Below is an amalgamated screen shot that shows the menu options available to admin and all-powerful host users. Below is a typical Text/HTML module shown with its container title - and a module menu visible to admin users with edit permissions. When you write a module, you can add functionality to the module Settings page, and also add items to the menu itself - in the same way as "Edit Text" as shown. &lt;/p&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/SLz1GqyVYpI/AAAAAAAAABg/M7ARkq91lMA/s1600-h/DNN_Menus.JPG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5241333561265840786" style="margin: 0px auto 10px; display: block; text-align: center;" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/SLz1GqyVYpI/AAAAAAAAABg/M7ARkq91lMA/s400/DNN_Menus.JPG" border="0" /&gt;&lt;/a&gt; DNN maintains a profile for each registered user, ie a set of properties of various types. As well as the basic profile properties of Username, FirstName, LastName, DisplayName and Email, DNN has many default properties for your Address, Contact Info, Biography, Time Zone and Preferred Locale. You can delete or add properties and mark individual properties as being required if desired. At program level, each user has a unique &lt;span style="color: rgb(0, 102, 0);"&gt;UserID&lt;/span&gt;; if a user joins with &lt;span style="color: rgb(0, 102, 0);"&gt;Username&lt;/span&gt; 'chris' then unregisters and subsequently someone else joins with the same &lt;span style="color: rgb(0, 102, 0);"&gt;Username&lt;/span&gt; 'chris', then these two users will have different &lt;span style="color: rgb(0, 102, 0);"&gt;UserID&lt;/span&gt;s.&lt;br /&gt;DNN has a rather fixed user interface for registration and setting your profile. For this reason, our &lt;a href="http://www.phdcc.com/phdcc.Data/"&gt;phdcc.Data&lt;/a&gt; module has a form generator that lets you build custom alternatives to these facilities.&lt;br /&gt;&lt;br /&gt;In conclusion, DNN provides a useful set of CMS/multi-user facilities for Microsoft-technology web sites. Any limitations can often be overcome using one of the many modules available. Everything DNN outputs is code and database driven which makes it harder to backup and understand what is going on. Next time, we'll look in detail at how to set up new skins and program modules.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-6091447768513337277?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/6091447768513337277/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=6091447768513337277' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6091447768513337277'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/6091447768513337277'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/09/dnn-introduction.html' title='DNN Introduction'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://4.bp.blogspot.com/_hkkBsHR0r5s/SLzz8Si4qWI/AAAAAAAAABI/BVAJGUEiG0Y/s72-c/DNN_PageLayout.PNG' height='72' width='72'/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-4667999752916587871</id><published>2008-07-08T09:34:00.004+01:00</published><updated>2008-09-06T14:32:21.614+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><category scheme='http://www.blogger.com/atom/ns#' term='skin'/><title type='text'>Simple DNN skin download for my VSJ article</title><content type='html'>An article I have written about DotNetNuke (DNN) has just appeared in print in July/August 2008 issue of &lt;a href="http://www.vsj.co.uk/articles/display.asp?id=736"&gt;Visual Systems Journal&lt;/a&gt; (VSJ).  The article appears in my blog in three entries: &lt;a href="http://chriscant.blogspot.com/2008/09/dnn-introduction.html"&gt;DNN Introduction&lt;/a&gt;, &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-skins.html"&gt;Writing DNN Skins&lt;/a&gt; and &lt;a href="http://chriscant.blogspot.com/2008/09/writing-dnn-custom-modules.html"&gt;Writing DNN custom modules&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Please read my blog article &lt;a href="http://chriscant.blogspot.com/2008/05/dnn-module-and-tab-module-settings.html"&gt;DNN module and tab-module settings&lt;/a&gt; for an article update on the different settings available in DNN module development.&lt;br /&gt;&lt;br /&gt;The simple skin and container can be downloaded &lt;a href="http://www.phdcc.com/download/DNN/simple.zip"&gt;here&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The phdcc.CodeModule module can be downloaded &lt;a href="http://dnn.phdcc.com/Downloads/tabid/54/Default.aspx"&gt;from here&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-4667999752916587871?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/4667999752916587871/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=4667999752916587871' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4667999752916587871'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4667999752916587871'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/07/simple-dnn-skin-download-for-my-vsj.html' title='Simple DNN skin download for my VSJ article'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5594179733075113754</id><published>2008-05-19T20:55:00.006+01:00</published><updated>2008-05-19T22:04:15.362+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='custom'/><category scheme='http://www.blogger.com/atom/ns#' term='settings'/><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><category scheme='http://www.blogger.com/atom/ns#' term='module'/><title type='text'>DNN module and tab-module settings</title><content type='html'>If you write a custom DotNetNuke (DNN) module, be careful with your use of the two types of settings that can be associated with an instance of a custom module - there are Module settings and Tab-Module settings. The difference becomes important (a) when you use Add Existing Module on another page and (b) when you Export Content from a module. In general, it makes sense to use the Module settings.&lt;br /&gt;&lt;br /&gt;The &lt;strong&gt;Module&lt;/strong&gt; settings are associated with all instances of a module on several pages, while the &lt;strong&gt;Tab-Module&lt;/strong&gt; settings are only associated with one instance (on a single page).&lt;br /&gt;&lt;br /&gt;When you copy a module instance to a new page with Add Existing Module, only the Module settings are available on the new page. So, if you want settings to be available after a module has been copied, then use Module settings, not Tab-Module settings.&lt;br /&gt;&lt;br /&gt;When a module's content is exported using Export Content, the module controller is called with the ModuleId only (ie without the TabModuleId). This only provides easy access to the Module settings.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Most of the following implementation examples require the use of the DNN ModuleController, obtained as follows in C#:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;  ModuleController objModules = new ModuleController();&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Most custom modules have a Settings user control that appears as a part of the module's main Settings page. When the user clicks on Update, the module setting's &lt;span style="font-family:courier new;"&gt;UpdateSettings()&lt;/span&gt; method is called. To save a Module setting, eg for a CssFile text box, use the following:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;  objModules.UpdateModuleSetting(ModuleId, "CssFile", txtCssFile.Text);&lt;/span&gt;&lt;br /&gt;If instead, you want to save a Tab-Module setting, use the following:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;  objModules.UpdateTabModuleSetting(TabModuleId, "CssFile", txtCssFile.Text);&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;The Settings user control is initialised using its &lt;span style="font-family:courier new;"&gt;LoadSettings()&lt;/span&gt; method. The ModuleSettingsBase &lt;span style="font-family:courier new;"&gt;Settings[]&lt;/span&gt; property provides access to the current settings, eg:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;  txtCssClass.Text = Settings["CssClass"] as string;&lt;/span&gt;&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;Settings[]&lt;/span&gt; contains both the Module and Tab-Module settings, with the Tab-Module settings taking precedence.&lt;br /&gt;&lt;br /&gt;Within your View control, the PortalModuleBase &lt;span style="font-family:courier new;"&gt;Settings[]&lt;/span&gt; property is the same.&lt;br /&gt;&lt;br /&gt;Other methods available are objModules.&lt;span style="font-family:courier new;"&gt;DeleteModuleSetting()&lt;/span&gt; and objModules.&lt;span style="font-family:courier new;"&gt;DeleteTabModuleSetting().&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;To implement Export Content, your module controller must implement IPortable. You must then write an &lt;span style="font-family:courier new;"&gt;ExportContent()&lt;/span&gt; method that might start as follows:&lt;br /&gt;&lt;span style="font-family:courier new;"&gt;public string ExportModule(int ModuleID)&lt;br /&gt;{&lt;br /&gt;  ModuleController objModules = new ModuleController();&lt;br /&gt;  Hashtable Settings = objModules.GetModuleSettings(ModuleID);&lt;br /&gt;  string CssFile = Settings["CssFile"] as string;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Several of my existing modules used Tab-Module settings. I have converted them to use Module settings. I was careful to transfer any existing settings from Tab-Module to Module. I used a "SettingsInModule" module setting set to "done" to indicate that the settings had been transferred. My conversion method was careful to work for brand new modules where no settings exist at all.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5594179733075113754?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5594179733075113754/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5594179733075113754' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5594179733075113754'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5594179733075113754'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/05/dnn-module-and-tab-module-settings.html' title='DNN module and tab-module settings'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>7</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1134348169627030251</id><published>2008-04-28T11:57:00.005+01:00</published><updated>2009-05-12T09:21:06.164+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='text size'/><category scheme='http://www.blogger.com/atom/ns#' term='Internet Explorer'/><category scheme='http://www.blogger.com/atom/ns#' term='IE'/><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='font'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DotNetNuke Internet Explorer font scaling</title><content type='html'>&lt;em&gt;Updated May 2009 to cope with MinimalEntropy skin.&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;When you put content onto a DotNetNuke (DNN) site you must think how other users see your information. By default, if your visitors use Internet Explorer (IE) then most of your site will NOT resize according to the user's Text Size setting. In my opinion this is not good practice because some users prefer larger text than others. This post shows how to make DNN fonts scale correctly. An &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font.html"&gt;earlier post&lt;/a&gt; shows what happens if you do not make my changes.&lt;br /&gt;&lt;br /&gt;In this post I make the changes in a portal.css stylesheet file - click here to &lt;a href="http://www.phdcc.com/download/DNN_ScalingFont_Portal_090511.css"&gt;download DNN_ScalingFont_Portal_090511.css&lt;/a&gt;. You may use and update this file freely provided you retain the attribution to Chris Cant. Note that this CSS file only affects the fonts shown to ordinary users - Edit and Admin fonts are unchanged.&lt;br /&gt;&lt;br /&gt;To use my CSS file, log in as an Administrator or Host then choose [Admin][Site Settings]. Scroll down and open up the [Stylesheet Editor]. Select all the existing text, copy and paste to a local text file as a backup. Insert my CSS file at the start of the style sheet textarea, then click on [Save Style Sheet]. To see the effect of the new file, you will have to navigate to a new page (eg Home) and refresh your browser (F5).&lt;br /&gt;&lt;br /&gt;Note that my CSS stylesheet is not guaranteed to make all fonts scale on your site - the current skin and container may have definitions that take precedence over the portal CSS. Please test this stylesheet carefully on all pages of your site before putting into production use.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;DNN uses several CSS stylesheets when rendering content. The following stylesheets are included in this order:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Portal default CSS (in /portals/_default/default.css) &lt;/li&gt;&lt;li&gt;Current skin CSS &lt;/li&gt;&lt;li&gt;Current container CSS &lt;/li&gt;&lt;li&gt;Portal CSS (eg in /Portals/0/portal.css)&lt;/li&gt;&lt;/ol&gt;The portal default CSS has fixed-size definitions for many generic HTML elements as well as many DNN-specific classes. My substitute Portal CSS overrides these definitions. Because the portal CSS is included last, it has a good chance of having its definitions used, though some skins and containers may have specific definitions that take precedence.&lt;br /&gt;&lt;br /&gt;The DNN Portal default CSS uses fixed size fonts, eg H1 has &lt;span style="font-family:courier new;"&gt;font-size:20px&lt;/span&gt;. This is fixed in my CSS by specifying the font size as &lt;span style="font-family:courier new;"&gt;font-size:x-large&lt;/span&gt; which lets IE resize the font as the user changes Text Size.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Font Family Definitions&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The first section of my CSS gathers all the font family definitions into one place. By altering the definitions here, you should change all the font families that are normally visible. The normal fonts are defined here as being &lt;span style="font-family:courier new;"&gt;Tahoma, Arial, Helvetica&lt;/span&gt;; the BLOCKQUOTE, PRE and CODE elements are &lt;span style="font-family:courier new;"&gt;Lucida Console, monospace&lt;/span&gt;. Alter all instances of both of these as you wish.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Scaling fonts and default weights/colours&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The next section of my CSS file does the main work of redefining generic HTML tags and DNN classes. The general HTML elements are put back to standard font size, colour and weight. Firefox (FF) and IE have different (but similar) defaults - the IE default is used.&lt;br /&gt;&lt;br /&gt;Next, various DNN classes are defined to use scaling font sizes.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Main menu and Module container menu scaling&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The main menu and module container menu are now made scaling. The main menu bar by default has a fixed size of 16px. This is changed to have a scaling size of 2em. The font sizes of the menu classes are defined as 1em. A similar job is done for the module container menu classes.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Other classes&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The existing style sheet has definitions for various classes used by the standard DNN template.  In many cases you can clear these definitions.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Example sites&lt;/strong&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="http://dnn.phdcc.com/"&gt;dnn.phdcc.com&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://www.identifynature.com/"&gt;http://www.identifynature.com/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://www.merchistontennis.net/" eudora="autourl"&gt;http://www.merchistontennis.net/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://www.festivaloffice.com/"&gt;http://www.festivaloffice.com/&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1134348169627030251?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1134348169627030251/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1134348169627030251' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1134348169627030251'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1134348169627030251'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/04/dotnetnuke-internet-explorer-font_28.html' title='DotNetNuke Internet Explorer font scaling'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5931883712376022819</id><published>2008-04-25T10:33:00.007+01:00</published><updated>2008-04-28T13:03:24.606+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='text size'/><category scheme='http://www.blogger.com/atom/ns#' term='Internet Explorer'/><category scheme='http://www.blogger.com/atom/ns#' term='IE'/><category scheme='http://www.blogger.com/atom/ns#' term='DotNetNuke'/><category scheme='http://www.blogger.com/atom/ns#' term='font'/><category scheme='http://www.blogger.com/atom/ns#' term='DNN'/><title type='text'>DotNetNuke Internet Explorer fixed size fonts</title><content type='html'>DotNetNuke (DNN) by default uses many fixed size fonts in its standard CSS styles. I show the effect of these fixed font sizes in Windows Internet Explorer (IE) as you change the text size from Smallest to Largest. Within Text/HTML modules, everything is fixed size except text within TD elements.&lt;br /&gt;&lt;br /&gt;DNN uses several CSS stylesheets when rendering content. The following stylesheets are included in this order:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Portal default CSS (in /portals/_default/default.css)&lt;/li&gt;&lt;li&gt;Current skin CSS &lt;/li&gt;&lt;li&gt;Current container CSS &lt;/li&gt;&lt;li&gt;Portal CSS (eg in /Portals/0/portal.css)&lt;/li&gt;&lt;/ol&gt;The Portal default CSS file has definitions for many generic HTML elements (as well as defining many DNN specific CSS classes). For example, tag H1 is defined as having &lt;span style="font-family:courier new;"&gt;font-size: 20px;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:lucida grande;"&gt;DNN defines fixed font sizes for these generic HTML elements:&lt;/span&gt;&lt;br /&gt;H1, H2, H3, H4, H5, H6, DT, TFOOT, THEAD, TH, SMALL, BIG&lt;br /&gt;&lt;br /&gt;&lt;span style="font-family:lucida grande;"&gt;There are two cases that I would like to highlight: (a) writing HTML within a Text/HTML module and (b) writing HTML within a custom module or in a skin/container.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;In the following examples, you will need to remember that the module output is encased first a module container and (outside that) the skin. In the test environment, both the skin and container ContentPane elements are TD elements.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;IE Scaling in a Text/HTML module&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;When you place a Text/HTML module on a page, the module always surrounds the entire text in a DIV element that has an attribute &lt;span style="font-family:courier new;"&gt;class="Normal"&lt;/span&gt;. The Normal class is defined in the default CSS as having a fixed point size (&lt;span style="font-family:courier new;"&gt;font-size: 11px;&lt;/span&gt;).&lt;br /&gt;&lt;br /&gt;The following image shows two screenshots that let you compare how Internet Explorer displays various tags with the Smallest font size on the left and the Largest font size on the right. As you can see, the only text that scales is within a TD tag.&lt;br /&gt;&lt;br /&gt;It is important to understand what is happening for the other elements. The P tag for instance does not have a definition in the default CSS. However it inherits its font size from its surrounding DIV tag which has class "Normal" that has a fixed size. For some reason, the TD tag does not inherit this font size.&lt;br /&gt;&lt;br /&gt;Note that other tags in the example have fixed font size definitions in the default CSS, eg H1 is defined as having &lt;span style="font-family:courier new;"&gt;font-size: 20px;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/SBGrM4PHHgI/AAAAAAAAAAo/1y_pTiypiQQ/s1600-h/DNN_TextModule_IE_FontChanges.JPG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5193120083077438978" style="MARGIN: 0px 10px 10px 0px; CURSOR: hand" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/SBGrM4PHHgI/AAAAAAAAAAo/1y_pTiypiQQ/s320/DNN_TextModule_IE_FontChanges.JPG" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;strong&gt;IE Scaling in a custom module or skin/container&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The following screenshot shows the same HTML - this time in a custom module (not Text/HTML). The output will be the same if you place the HTML within a skin or container.&lt;br /&gt;&lt;br /&gt;In this case, the HTML is NOT surrounded by a DIV element and so the HTML does not inherit the Normal class. This time, the P, TD, DIV, DD, BLOCKQUOTE and PRE tags scale as you change the IE font size. This is because all these elements do not have a (fixed size) definition within the DNN default CSS. All the other elements stay the same size because they do have fixed size CSS definitions.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/SBGrM4PHHhI/AAAAAAAAAAw/Z68LKMkvE90/s1600-h/DNN_CustomModule_IE_FontChanges.JPG"&gt;&lt;img id="BLOGGER_PHOTO_ID_5193120083077438994" style="MARGIN: 0px 10px 10px 0px; CURSOR: hand" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/SBGrM4PHHhI/AAAAAAAAAAw/Z68LKMkvE90/s320/DNN_CustomModule_IE_FontChanges.JPG" border="0" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;When you put content onto a DNN page you must think how other users see your information. If your visitors use Internet Explorer then most of your site will NOT resize according to the user's Text Size setting. In my opinion this is not the recommended practice because some users prefer larger text than others - more on how to make all your text resize in &lt;a href="http://chriscant.blogspot.com/2008/04/dotnetnuke-internet-explorer-font_28.html"&gt;my next blog entry&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The Firefox Text Size and Internet Explorer Zoom Level options DO alter the size of fixed size fonts.&lt;br /&gt;&lt;br /&gt;To achieve a consistent fixed size effect you need to provide a font size definition for any TD elements you use within Text/HTML modules. For text in other modules or skins, you need to think carefully about which elements scale and which do not.&lt;br /&gt;&lt;br /&gt;To change the sizes of the fonts used for various elements, you can alter any of the CSS files mentioned at the top of this post - typically it will be easiest to alter your Post CSS file.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5931883712376022819?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5931883712376022819/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5931883712376022819' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5931883712376022819'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5931883712376022819'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/04/dotnetnuke-internet-explorer-font.html' title='DotNetNuke Internet Explorer fixed size fonts'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_hkkBsHR0r5s/SBGrM4PHHgI/AAAAAAAAAAo/1y_pTiypiQQ/s72-c/DNN_TextModule_IE_FontChanges.JPG' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-897587675680806305</id><published>2008-03-09T16:26:00.003Z</published><updated>2008-03-09T16:39:01.944Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='ASP.NET'/><category scheme='http://www.blogger.com/atom/ns#' term='ViewState'/><title type='text'>ViewState in ASP.NET Embedded Code Blocks</title><content type='html'>This morning I had trouble getting an ASP.NET user control to access its ViewState on postback. The crucial trick was to populate control items in a Page_Load function not in an embedded code block. The reason is because the code block is only run at render time which happens after the ViewState has been saved.&lt;br /&gt;&lt;br /&gt;(I had been loading the user control dynamically using LoadControl, but that turned out to be not relevant.)&lt;br /&gt;&lt;br /&gt;Now, within a user control, suppose I have a wee label and an ineffectual button:&lt;br /&gt;&lt;code&gt;&amp;lt;asp:label id="saver" runat="server" /&amp;gt;&lt;br /&gt;&amp;lt;asp:button id="Button1" runat="server" text="Go" /&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;If the label is set using an embedded code block&lt;br /&gt;&lt;code &gt;&amp;lt;% if (!IsPostBack) saver.Text = "Am I saved"; %&amp;gt;&lt;/code&gt;&lt;br /&gt;then it is not preserved/shown after the "Go" button has been pressed.&lt;br /&gt;&lt;br /&gt;However, if it is set in Page_Load then the label text is shown correctly after "Go" is pressed.&lt;br /&gt;&lt;pre&gt;protected void Page_Load(object sender, EventArgs e)&lt;br /&gt;{&lt;br /&gt;  if (!IsPostBack)&lt;br /&gt;    saver.Text = "Am I saved";&lt;br /&gt;}&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-897587675680806305?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/897587675680806305/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=897587675680806305' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/897587675680806305'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/897587675680806305'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2008/03/viewstate-in-aspnet-embedded-code.html' title='ViewState in ASP.NET Embedded Code Blocks'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2574402088988946960</id><published>2007-02-10T12:51:00.000Z</published><updated>2007-02-10T13:02:21.747Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='JavaScript Object Notation'/><category scheme='http://www.blogger.com/atom/ns#' term='JSON'/><category scheme='http://www.blogger.com/atom/ns#' term='Ajax'/><title type='text'>JSON web apps without Ajax</title><content type='html'>This article first appeared in the December 2006 issue of &lt;a href="http://www.vsj.co.uk/"&gt;Visual Systems Journal (VSJ&lt;/a&gt;).&lt;br /&gt;&lt;br /&gt;In the July/August 2006 issue of VSJ, Mike James pondered how best to design new web applications.  Good responsiveness means that more code has to be moved client-side into the browser, making Ajax requests to the server and updating the page on the fly or in response to user actions.  However, good design also implies a separation between presentation HTML, presentation code, business logic and the data handling layers, as is possible with various server-side technologies such as ASP.NET.&lt;br /&gt;&lt;br /&gt;My idea presented here is to keep the client-side code as separate from the server-side code as possible, and to keep on using the good design practices techniques server-side.&lt;br /&gt;&lt;br /&gt;If you generate Ajax requests yourself and update the page DOM, then the data can get out of sync with the server-side representation.  For example, in ASP.NET a server-side variable &amp;lt;asp::Label .. /&amp;gt; is rendered as an HTML &amp;lt;SPAN&amp;gt;; if you update the SPAN in JavaScript then the new value is not necessarily present server-side at the next post-back.  I tried to resolve this a few months back and got in a terrible mess, so I gave up.  Nowadays, for ASP.NET, various frameworks such as Atlas hide these difficulties; essentially you carry on coding server-side as usual and the framework generates the right JavaScript and Ajax behind the scenes to improve responsiveness for your GridView or whatever.&lt;br /&gt;&lt;br /&gt;My most recent project required good interactivity on the client-side but also access to good programmability and a database on the server.  On the server I decided to stick with what I now know best, ASP.NET and SQL Server.  Using stored procedures effectively provided a data access API, as well as improving security.  I also used the ASP.NET membership and profile features which provided various levels of abstraction above SQL Server database views, stored procedure and tables.  My data tables were keyed off the ASP.NET user information, so my stored procedures looked up some data in the ASP.NET membership tables.&lt;br /&gt;&lt;br /&gt;There are two client applications: a specialised photo viewer and photo designer.  These did not correspond to any available server controls so there was no quick way to implement these with high responsiveness.&lt;br /&gt;&lt;br /&gt;My first reaction was to make Ajax requests to find which photo to show next.  However I eventually realised that the client apps do not need to ask the server for this information because it could be included with the page when it is loaded.  This makes the pay-load for the page a bit bigger but the pay-off is a better response time because it does not have to query the server - this also reduces the load on the server.  This approach also gives better compatibility with some older browsers that do not support Ajax requests.&lt;br /&gt;&lt;br /&gt;The design could be summed up as having client applications that were largely separate from the server code.  Most pages on the site could be handled by standard ASP.NET code using the full monty of server-side facilities.  However the crucial client applications were kept standalone as far as possible - this should also help to achieve the aim of operating standalone off-site or as a mashup on other pages.&lt;br /&gt;&lt;br /&gt;I had previously decided that the photo information should be stored on the server in an XML file.  Some of this information is also contained in the site database, but I thought that it was important to have all the photo-site data together in one easy-to-use place.  As a consequence there is a need to maintain consistency between the database and the XML, eg if a user deletes a photo then the database entry is removed; the XML also needs to be updated.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Client photo viewer app&lt;/h3&gt;&lt;br /&gt;&lt;br /&gt;Anyway, I needed to pass the XML data to the web page so that the photo viewer client app knew what to do.  I decided to do this by converting the XML to JSON and passing it to the client app JavaScript.  In the codebehind C# for the ASP.NET page, I load the XML and convert it to JSON using the code I described in last month's article.  The JSON is passed to the page using a ClientScriptManager RegisterStartupScript call which eventually invokes the page processJSON() JavaScript function.  The one trick to this technique is to realise that the JSON is interpreted twice, once when processJSON() is called, and again when the JSON parser is called.  To resolve this issue, simply replace each \ with a \\ before sending the JSON string off.&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:blue;"&gt;    XmlDocument doc = new XmlDocument();&lt;br /&gt;    doc.LoadXml("&amp;lt;whatever.xml&amp;gt;");&lt;br /&gt;    string JSON = XmlToJSON(doc);&lt;br /&gt;    JSON = JSON.Replace(@"\", @"\\");&lt;br /&gt;    ClientScriptManager cs = Page.ClientScript;&lt;br /&gt;    cs.RegisterStartupScript(GetType(), "Startup", "processJSON('" + JSON + "');", true);&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;ASP.NET inserts the processJSON() call at the end of the web page so that it is called after almost everything else has been loaded.  I'm not sure if this corresponds exactly to the BODY onload event, but it seems to work OK.&lt;br /&gt;&lt;br /&gt;The page HTML should include any JavaScript source files that the project requires:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:teal;"&gt;    &amp;lt;script src="json.js" type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;    &amp;lt;script src="ClientApp.js" type="text/javascript"&amp;gt;&amp;lt;/script&amp;gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;The ClientApp.js should process the JSON as is appropriate for your application:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;    var obj;&lt;br /&gt;    function processJSON( JSON)&lt;br /&gt;    {&lt;br /&gt;        obj = JSON.parseJSON();&lt;br /&gt;        if( !obj)&lt;br /&gt;        {&lt;br /&gt;            alert("JSON decode error");&lt;br /&gt;            return;&lt;br /&gt;        }&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Client designer app&lt;/h3&gt;&lt;br /&gt;&lt;br /&gt;The photo viewer client-side app does not need to interact with server-side code (all it does is request new images) although it could be enhanced to log the actions that a user has taken.  However the matching designer client-side app does need to interact with the server to store the design that a user is making.  I hold any pending user changes in a JavaScript array and report them to the server when the user clicks on the Done button or opts to move to a new photo.  There is a visual indication when there are unsaved changes; if the user navigates away then any unsaved changes are lost.&lt;br /&gt;&lt;br /&gt;The changes are reported by storing them in JSON format in a hidden form variable called Changes.  This code shows how the Done button and Changes are defined in the .aspx file:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:teal;"&gt;    &amp;lt;asp:Button ID="btnDone" Text="Done" OnClick="btnDone_Click" runat="server" /&amp;gt;&lt;br /&gt;    &amp;lt;input id="Changes" type="hidden" runat="server" /&amp;gt; &lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;In case you are not familiar with this syntax, btnDone and Changes are page variables that can used in server-side code.  When the ASPX page is rendered, they are converted into HTML that can be accessed using JavaScript and the DOM.  Note that when rendered, the DOM ids may be different, eg if the page uses a master page layout.&lt;br /&gt;&lt;br /&gt;If the user clicks on Done, I want to store any changes before posting back to the server, so I add an onclick handler to the Done button in the .aspx.cs Page_Load():&lt;br /&gt;&lt;br /&gt;    &lt;pre style="color:blue;"&gt;btnDone.Attributes["onclick"] = "javascript:return btnDone_OnClick()";&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;When the page is rendered, btnDone becomes an INPUT form field with its onclick handler set.  The JavaScript onclick handler stores the JSON string version of the ChangesDone variable and then returns true to let the page form be submitted:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;    function btnDone_OnClick()&lt;br /&gt;    {&lt;br /&gt;        ChangesField.value = ChangesDone.toJSONString();&lt;br /&gt;        return true;    // Don't cancel btnDone form submit&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;For the above to work, the JavaScript ChangesField variable needs to be set correctly to the Changes hidden field.  Remember that Changes runs server-side as well.  This means that ASP.NET may have decorated the generated HTML field name with various prefixes, eg if the page uses a master page layout.  To get the correct name, my aspx.cs code registers some startup script to tell the JavaScript the correct name for Changes.ClientID.  The JavaScript also needs the btnDone.UniqueID so this is passed as well:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:blue;"&gt;    ClientScriptManager cs = Page.ClientScript;&lt;br /&gt;    cs.RegisterStartupScript(GetType(), "SetServerControls", "SetServerControls("+&lt;br /&gt;        "'" + Changes.ClientID + "','" + btnDone.UniqueID + "');", true);&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;This is the JavaScript code that is called at startup to store the control names:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;    var ChangesField = false;&lt;br /&gt;    var btnDoneUniqueId = false;&lt;br /&gt;&lt;br /&gt;    function SetServerControls( ChangesField_ClientID, _btnDoneUniqueId)&lt;br /&gt;    {&lt;br /&gt;        ChangesField = document.getElementById(ChangesField_ClientID);&lt;br /&gt;        btnDoneUniqueId = _btnDoneUniqueId;&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;I mentioned earlier that I also wanted to postback to the server on events other than pressing the Done button.  This is achieved in the JavaScript handler for each event by storing the ChangesDone data and then simulating a press of the Done button.  This is done by calling the __doPostBack() function that ASP.NET will have generated, passing the required Done button UniqueId, eg:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;    function Move_dblclk()&lt;br /&gt;    {&lt;br /&gt;        ChangesField.value = ChangesDone.toJSONString();&lt;br /&gt;        __doPostBack(btnDoneUniqueId,'');&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;OK, let's move back to the server and see how the Done click is handled server-side.  This simply picks up the Changes value and decodes it using the Nii.JSON.JSONArray parser along the following lines:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:blue;"&gt;    protected void btnDone_Click(object sender, EventArgs e)&lt;br /&gt;    {&lt;br /&gt;        string sChanges = Changes.Value;&lt;br /&gt;        if (!string.IsNullOrEmpty(sChanges))&lt;br /&gt;        {&lt;br /&gt;            JSONArray Changes = new JSONArray(sChanges);&lt;br /&gt;        }&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;There is one complication to the above process for my designer application.  In the viewer, the startup JavaScript can be registered in the Page_Load() method.  However the Done button handler btnDone_Click() will be called after Page_Load().  If the Done handler alters the information that you want to send to the client, then you must generate the startup JavaScript later.  I found that this could be done in the page prerender function Page_Prerender() as follows:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:blue;"&gt;protected void Page_Prerender(object sender, EventArgs e)&lt;br /&gt;{&lt;br /&gt;    string JSON = XmlToJSON(SiteXmlDocument);&lt;br /&gt;    JSON = JSON.Replace(@"\", @"\\");&lt;br /&gt;    ClientScriptManager cs = Page.ClientScript;&lt;br /&gt;    cs.RegisterStartupScript(GetType(), "SetServerControls", "SetServerControls("+&lt;br /&gt;        "'" + Changes.ClientID + "','" + btnDone.UniqueID + "');", true);&lt;br /&gt;    cs.RegisterStartupScript(GetType(), "Startup", "processJSON('" + JSON + "');", true);&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;On the server, I made the viewer and designer into .ascx user controls.  Incidentally the standard Visual Studio File in Files did not find text in C# .ascx.cs files even though the filter includes *.cs.  I reported this a bug but Microsoft bizarrely said that this was by design; fix it by adding *.ascx.cs to the filter.  I also add *.js to find text in JavaScript and *.master to find master page markup.&lt;br /&gt;&lt;br /&gt;Here's another JavaScript tip: include a version number in the filename, eg ClientApp_v1_01.js if the code ever changes.  If you don't do this, a returning user's browser may use a cached and out-of-date version.  When you do a change, rename the file and update all the code that references it.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Conclusion&lt;/h3&gt;&lt;br /&gt;&lt;br /&gt;Whew!  But not too bad really.  With this tricky code out of the way, I could concentrate on the client and server code separately which I think was a wise choice.  Coming back to JavaScript for a large project was not as bad as I thought it might be, as it was more expressive than I had realised and DOM interactions worked well and pretty consistently across most browsers.  However I did revert to alert() box debugging.  A search found various JavaScript debuggers but I never got round to trying them.  For JavaScript, ASP.NET and other technologies, I refer quite often to help sites on the web, so thanks for all the fish.&lt;br /&gt;&lt;br /&gt;On the server, separation of the codebehind from the presentation HTML is definitely a good thing.  If I am honest however, I have to report that my .aspx files still contain a tiny amount of inline code to access certain page fields or fill GridView TemplateFields.  Declarative design can only ever take you so far, so my .aspx.cs code is largely linked to the presentation HTML, eg filling labels or coping with SelectedIndexChanged.  I probably do not make enough of an effort to separate the business logic from the presentation logic, although using stored procedures as a data access layer is a good discipline for security as well as design-separation reasons.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2574402088988946960?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2574402088988946960/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2574402088988946960' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2574402088988946960'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2574402088988946960'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2007/02/json-web-apps-without-ajax.html' title='JSON web apps without Ajax'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-4226508992828231859</id><published>2007-01-04T21:44:00.000Z</published><updated>2007-01-05T11:16:05.752Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='JavaScript Object Notation'/><category scheme='http://www.blogger.com/atom/ns#' term='JSON'/><category scheme='http://www.blogger.com/atom/ns#' term='Ajax'/><title type='text'>JavaScript Object Notation (JSON)</title><content type='html'>This article first appeared in the November 2006 issue of &lt;a href="http://www.vsj.co.uk" target="_blank"&gt;Visual Systems Journal (VSJ)&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;In the July/August 2006 issue of VSJ, Mike James showed the basics of how to make Asynchronous JavaScript and XML (Ajax) calls from a web page to a server using an XMLHttpRequest object.  This returns either plain text in &lt;code&gt;responseText&lt;/code&gt; or XML in &lt;code&gt;responseXML&lt;/code&gt; properties.  Unfortunately, XML can be a little cumbersome to use, which isn't going to make your JavaScript any easier to understand.  However there is an alternative in the form of JavaScript Object Notation (JSON, pronounced Jason) which is a string representation of structured data using JavaScript's object literal notation, ie name/value pair objects.&lt;br /&gt;&lt;br /&gt;Here's some XML:&lt;br /&gt;&lt;pre&gt;&amp;lt;?xml version='1.0' encoding='UTF-8'?&amp;gt;&lt;br /&gt;&amp;lt;business&amp;gt;&lt;br /&gt; &amp;lt;name&amp;gt;A Bee Co&amp;lt;/name&amp;gt;&lt;br /&gt; &amp;lt;contact type='email'&amp;gt;sales@abee.co.uk&amp;lt;/contact&amp;gt;&lt;br /&gt; &amp;lt;employee type='Manager'&amp;gt;&lt;br /&gt;  Alison Bee&lt;br /&gt; &amp;lt;/employee&amp;gt;&lt;br /&gt; &amp;lt;employee type='worker'&amp;gt;&lt;br /&gt;  John Sloop&lt;br /&gt; &amp;lt;/employee&amp;gt;&lt;br /&gt;&amp;lt;/business&amp;gt;&lt;/pre&gt;&lt;br /&gt;In JSON the same data might look like this:&lt;br /&gt;&lt;pre&gt;{ "business":&lt;br /&gt;    { "name": "A Bee Co",&lt;br /&gt;      "contact": {"type": "email", "value": "sales@abee.co.uk" },&lt;br /&gt;      "employee": [ {"type": "Manager", "value": "Alison Bee" },&lt;br /&gt;                    {"type": "worker", "value": "John Sloop" }&lt;br /&gt;                  ]&lt;br /&gt;    }&lt;br /&gt;}&lt;/pre&gt;&lt;br /&gt;If the above JSON string is in a var called &lt;code&gt;Business&lt;/code&gt;, then it can be parsed very simply in JavaScript:&lt;br /&gt;&lt;pre style="color:purple;"&gt;    var biz = eval("(" + Business + ")");&lt;/pre&gt;&lt;br /&gt;The following easy-to-use values are then available:&lt;br /&gt;&amp;bull;&amp;nbsp; biz.business.name: &lt;code&gt;A Bee Co&lt;/code&gt;&lt;br /&gt;&amp;bull;&amp;nbsp; biz.business.contact.type: &lt;code&gt;email&lt;/code&gt;&lt;br /&gt;&amp;bull;&amp;nbsp; biz.business.employee.length: &lt;code&gt;2&lt;/code&gt;&lt;br /&gt;&amp;bull;&amp;nbsp; biz.business.employee[0].value: &lt;code&gt;Alison Bee&lt;/code&gt;&lt;br /&gt;As you can see, the JSON string has been converted into an object tree.  If you remove the superfluous "business" object from the JSON, then the values would be even simpler, eg biz.name.&lt;br /&gt;&lt;br /&gt;Note that the multiple "employee" XML elements were translated into a JSON/JavaScript array.  If there were just one employee, then biz.business.employee would simply be a property string.  To cope with this, I use my &lt;code&gt;ObjectToArray&lt;/code&gt; function to convert this property into an array which lets me handle it more simply:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;function ObjectToArray( obj)&lt;br /&gt;{&lt;br /&gt;    if( !obj) return new Array(); // zero element array&lt;br /&gt;    if( !obj.length) return new Array(obj); // one element array&lt;br /&gt;    return obj; // array already&lt;br /&gt;}&lt;br /&gt;var employees_array = ObjectToArray(biz.business.employee);&lt;/pre&gt;&lt;br /&gt;This approach works well as long as you know the structure that you are expecting, which will usually be the case.  Obviously you will then want to do something with your data, ie show it to the user.  Be careful if you are using server-side processing such as ASP.NET because changing a page object will not necessarily change the value seen at the next real postback, or worse, validation errors can occur.  Handling interactions between client and server variables can be a messy kettle of fish which I don't want to go into here, but can be solved using Atlas and other Ajax frameworks.  In next month's article I suggest a simple client-server scenario which makes use of JSON, XML, JavaScript and ASP.NET2.&lt;br /&gt;&lt;br /&gt;Anyway, back at the bit-face, you can find out more about JSON at www.json.org.  JSON supports these types of values: string, number, object, array, boolean and null.  Strings are in Unicode and various escape sequences are supported.  JSON doesn't support binary data so you cannot embed images.  JSON seems to be supported by all versions of JavaScript in browsers.&lt;br /&gt;&lt;br /&gt;JSON can be used in other languages apart from JavaScript.  It's main use seems to be data-interchange, particularly in Ajax requests.  Note that Ajax requests are not supported by older browsers.  Although ease of use was the original raison d'etre for JSON, it is now also commonly used because it is more compact than XML resulting in reduced data transfer requirements.&lt;br /&gt;&lt;br /&gt;Parsing time is also reputedly less for JSON.  However you need to be careful using eval() against an untrusted source.  The JSON web site provide a file json.js that includes a safer parser string method prototype &lt;code&gt;parseJSON()&lt;/code&gt;, so replace the above eval() call with:&lt;br /&gt;&lt;pre style="color:purple;"&gt;var biz = Business.parseJSON();&lt;/pre&gt;&lt;br /&gt;This still doesn't stop potentially dangerous values appearing as parsed values, so be careful if you stuff values into DOM &lt;code&gt;innerHTML&lt;/code&gt; values for example.  The JSON JavaScript code also defines &lt;code&gt;toJSONString()&lt;/code&gt; to convert JavaScript objects and arrays into JSON.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;C# XML to JSON conversion&lt;/h3&gt;&lt;br /&gt;In a recent web application I decided to store and process data server-side in XML because this is a standard storage type and ASP.NET2 provides good routines for handling an XmlDocument in memory.  However I wanted to transfer information to the web page client in JSON, and decode returned information in JSON.&lt;br /&gt;&lt;br /&gt;Good old www.json.org provided classes to convert JSON into C# objects at www.json.org/cs.zip.  As an example, in JavaScript convert your employee array to a JSON string:&lt;br /&gt;&lt;br /&gt;&lt;pre style="color:purple;"&gt;var JSON_employees = biz.business.employee.toJSONString();&lt;/pre&gt;&lt;br /&gt;Once this is safely server-side, use C# code like this to retrieve the name/value pairs for each employee, slightly laboriously:&lt;br /&gt;&lt;pre style="color:blue;"&gt;    using Nii.JSON;&lt;br /&gt;    JSONArray employees = new JSONArray(JSON_employees);&lt;br /&gt;    for (int eno = 0; eno &lt; employees.Count; eno++)&lt;br /&gt;    {&lt;br /&gt;        JSONObject jemployee = (JSONObject)employees[eno];&lt;br /&gt;        for (int ano = 0; ano &lt; jemployee.Count; ano++)&lt;br /&gt;        {&lt;br /&gt;            string name = jemployee[ano].ToLower();&lt;br /&gt;            string value = jemployee[name].ToString();&lt;br /&gt;        }&lt;br /&gt;    }&lt;/pre&gt;&lt;br /&gt;I couldn't find a means of converting XML into JSON in C#, so I thought that it must be straight forward to write my own converter.  It turned out that there were more XML scenarios than I had originally envisaged.  For simple XML elements containing text there seems an obvious translation:&lt;br /&gt;&lt;pre&gt; XML: &amp;lt;xx&amp;gt;yyy&amp;lt;/xx&amp;gt;&lt;br /&gt; JSON: "xx":"yyy"&lt;/pre&gt;&lt;br /&gt;If the XML element has attributes or contains child elements then these are converted into object name/value pairs, with text usually converted into a field called "value".  (Life gets complicated if there is an attribute or child elements called "value".)&lt;br /&gt;&lt;pre&gt; XML: &amp;lt;xx w='zzz'&amp;gt;&amp;lt;aa&amp;gt;bb&amp;lt;/aa&amp;gt;yyy&amp;lt;/xx&amp;gt;&lt;br /&gt; JSON: "xx": { "aa":"bb", "w":"zzz", "value":"yyy" }&lt;/pre&gt;&lt;br /&gt;Duplicate elements are converted into an array:&lt;br /&gt;&lt;pre&gt; XML: &amp;lt;xx&amp;gt;&amp;lt;aa&amp;gt;bb&amp;lt;/aa&amp;gt;&amp;lt;aa&amp;gt;cc&amp;lt;/aa&amp;gt;&amp;lt;/xx&amp;gt;&lt;br /&gt; JSON: "xx": { "aa": ["bb","cc"] }&lt;/pre&gt;&lt;br /&gt;One example of "equivalent" XML and JSON online cleverly contracted this:&lt;br /&gt;&lt;pre&gt; XML: &amp;lt;employees&gt;&amp;lt;employee&amp;gt;bb&amp;lt;/employee&amp;gt;&amp;lt;employee&amp;gt;cc&amp;lt;/employee&amp;gt;&amp;lt;/employees&amp;gt;&lt;br /&gt; JSON: "employees": ["bb","cc"]&lt;/pre&gt;&lt;br /&gt;Note that XML and JSON conversions are not necessarily reversible, ie you cannot convert one to the other and back again and guarantee to get the same as you started.&lt;br /&gt;&lt;br /&gt;You can get my code online &lt;a href="http://www.phdcc.com/xml2json.htm"&gt;www.phdcc.com/xml2json.htm&lt;/a&gt;.  The code also makes sure that characters are escaped if necessary for correct representation in JSON.&lt;br /&gt;&lt;br /&gt;Resources:&lt;br /&gt;&lt;a href="http://www.json.org" target="_blank"&gt;www.json.org&lt;/a&gt;, www.json.org/js.html, www.json.org/cs.zip, www.crockford.com, www.phdcc.com/xml2json.htm&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-4226508992828231859?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/4226508992828231859/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=4226508992828231859' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4226508992828231859'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/4226508992828231859'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2007/01/javascript-object-notation-json.html' title='JavaScript Object Notation (JSON)'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3807735361710926453</id><published>2006-12-18T11:22:00.000Z</published><updated>2006-12-18T11:48:55.801Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='TIFF'/><category scheme='http://www.blogger.com/atom/ns#' term='Image'/><category scheme='http://www.blogger.com/atom/ns#' term='XMP'/><category scheme='http://www.blogger.com/atom/ns#' term='JPEG'/><title type='text'>Vista Image XMP Tags</title><content type='html'>Microsoft Windows Vista lets you set various meta-data properties for image files, ie JPEG and TIFF files.  A new feature is to add one or more Tags to a picture.&lt;br /&gt;&lt;br /&gt;Previously, meta-data information was added to image files using EXIF (Exchangeable Image File Format).  However Vista adds image meta-data using XMP (Extensible Metadata Platform) - this is XML-formatted information embedded in the image file.  &lt;a href="http://partners.adobe.com/public/developer/en/xmp/sdk/XMPspecification.pdf"&gt;Click here to get the XMP spec.&lt;/a&gt;  Where possible the meta-data is also stored in the corresponding EXIF data.&lt;br /&gt;&lt;br /&gt;Using the ASP.NET System.Drawing.Image class, it did not seem to be possible to extract the XMP data in a JPEG file.  The XMP is stored in an APP1 section.  To find it, I therefore did a somewhat nasty binary scan for these 29 characters &lt;code&gt;http://ns.adobe.com/xap/1.0/&lt;/code&gt; and then checked for the APP1 marker 0xFFE1 two words before.  The first word beforehand then contains APP1 size.  The XMP Packet byte[] can then be extracted and decoded from UTF-8.&lt;br /&gt;&lt;br /&gt;For TIFF files, System.Drawing.Image returns the XMP byte[] data as property id 700.&lt;br /&gt;&lt;br /&gt;For both file types, the XMP Packet can then be loaded using System.Xml.XmlDocument.  The actual XML lets you extract data from these fields (the Vista usage is in brackets):&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;dc:subject (Tags)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dc:title (Title)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dc:creator (Author)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dc:description (Subject)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dc:rights (Copyright)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;tiff:artist (Author)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;exif:UserComment (Comments)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;xmp:Rating (Star rating number)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;MicrosoftPhoto:LastKeywordXMP (seems to the same as dc:subject)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;MicrosoftPhoto:Rating (not sure what this is)&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;The actual field values are in plain text, rdf:Bag, rdf:Alt or rdf:Seq lists.&lt;br /&gt;&lt;br /&gt;I did this work to add the meta-data information to that found by my &lt;a href="http://www.phdcc.com/findinsite/"&gt;ASP.NET search engine FindinSite-MS&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3807735361710926453?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3807735361710926453/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3807735361710926453' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3807735361710926453'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3807735361710926453'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/12/vista-image-xmp-tags.html' title='Vista Image XMP Tags'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2611682393742857231</id><published>2006-12-11T13:38:00.000Z</published><updated>2006-12-11T16:15:51.846Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='BCD'/><category scheme='http://www.blogger.com/atom/ns#' term='Multiboot'/><category scheme='http://www.blogger.com/atom/ns#' term='Vista'/><category scheme='http://www.blogger.com/atom/ns#' term='EasyBCD'/><category scheme='http://www.blogger.com/atom/ns#' term='boot'/><title type='text'>Microsoft Windows Vista Multi-boot</title><content type='html'>I got a new hard disk to work with Vista (as well as to cope with bulging photo files etc).  I currently use HyperOS to multiboot between various Windows systems - one on each partition - although I virtually never boot into systems prior to XP nowadays.&lt;br /&gt;&lt;br /&gt;I installed the new Barracuda drive as the second drive on the system and partitioned it into 5 chunks using Seagate's DiscWizard, ie one primary partition and 4 logical partitions (in an extended partition).&lt;br /&gt;&lt;br /&gt;After a bit of heartache I am now in the position where the Vista bootloader presents these three options at startup:&lt;br /&gt;* Earlier version of Windows&lt;br /&gt;* Microsoft Windows Vista&lt;br /&gt;* 95 98 ME Systems&lt;br /&gt;The "Earlier version of Windows" option boots into the current HyperOS-selected version of Windows.&lt;br /&gt;&lt;br /&gt;The main point to notice is that when in XP I have various drives on Disk 1 labelled C: to M: with Disk 2 showing up as N: to R: and DVD drives as S: and T:.&lt;br /&gt;However when in Vista, Disk 2 shows up as C: to G:, the DVDs as H: and I: and Disk 1 as J: onwards.  This scenario might require careful handling if you have shared data or batch files etc.&lt;br /&gt;&lt;br /&gt;To get to this happy situation required some work.  An initial new install of Vista from one of the XP instances led to a hung boot.  After getting over the initial panic, changing the BIOS boot order to boot on an XP boot CD let me go into a Recovery Console.  The fixmbr and fixboot commands restored the old status quo.&lt;br /&gt;&lt;br /&gt;(Each disk has a Master Boot Record (MBR) with load code and basic partition information.  In addition, each partition has a volume boot record that contains the real OS loader.  The fixmbr and fixboot commands restore the XP loader (NTLDR) for these boot records.  The HyperOS cobblerx command also restores the XP MBR.)&lt;br /&gt;&lt;br /&gt;The Vista boot loader (BCD) has a new MBR and volume loader (WINLOAD).  The load information is stored in the C:\BOOT\ directory in the BCD file.&lt;br /&gt;&lt;br /&gt;An easy way to cope with the BCD from XP and Vista is to use &lt;a href="http://neosmart.net/dl.php?id=1" target="_blank"&gt;NeoSmart's EasyBCD tool&lt;/a&gt; - it seems like you must have the .NET framework 2 installed to use EasyBCD.&lt;br /&gt;&lt;br /&gt;Perhaps the trick to installing Vista might have been to *first* use EasyBCD to install the Vista Master Boot Loader before starting the install.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2611682393742857231?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2611682393742857231/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2611682393742857231' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2611682393742857231'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2611682393742857231'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/12/microsoft-windows-vista-multi-boot.html' title='Microsoft Windows Vista Multi-boot'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-9111175086998432921</id><published>2006-10-19T08:31:00.000+01:00</published><updated>2006-10-19T09:04:47.259+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='Google'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Google gadget on your webpage</title><content type='html'>Google gadgets can now be placed on your own web pages, taking the wind out the sails of Widgetbox etc.  Previously, Google gadgets could only be places on your personalised Google home page, a Google pages site or the Google Desktop Sidebar.&lt;br /&gt;&lt;br /&gt;For a gadget that appears in the Google-Gadgets-for-your-Webpage directory, such as my &lt;a href="http://www.google.com/ig/directory?synd=open&amp;url=http://www.spacebrowse.com/gadget/SpaceBrowse.xml"&gt;Space Browse virtual tour viewer&lt;/a&gt;, click on the "Add to your webpage" button.  You are then taken to a page where you can choose various display and gadget settings.  It is often important to choose a suitable display width and height.  For Space Browse, choose 300x260 pixels.&lt;br /&gt;&lt;br /&gt;Then click "Get the Code" to show a textbox containing the HTML to copy and paste into your web page.  For the Space Browse gadget, this looks like this, with all your settings encoded as parameters:&lt;br /&gt;&lt;code&gt;&amp;lt;script src="http://gmodules.com/ig/ifr?url=http://www.spacebrowse.com/gadget/SpaceBrowse.xml&amp;up_SiteNo=&amp;up_ShowMode=Random&amp;up_UserName=&amp;up_Search=&amp;synd=open&amp;w=300&amp;h=260&amp;title=Space+Browse+virtual+tour&amp;border=%23ffffff%7C3px%2C1px+solid+%23999999&amp;output=js"&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;You then simply have to cut and paste this HTML code onto your web page.  For some sites such as on a blogsite Blogger template, you will have to replace the &amp;amp; characters with &amp;amp;amp; as follows:&lt;br /&gt;&lt;code&gt;&amp;lt;script src='http://gmodules.com/ig/ifr?url=http://www.spacebrowse.com/gadget/SpaceBrowse.xml&amp;amp;amp;up_SiteNo=&amp;amp;amp;up_ShowMode=Random&amp;amp;amp;up_UserName=&amp;amp;amp;up_Search=&amp;amp;amp;synd=open&amp;amp;amp;w=300&amp;amp;amp;h=260&amp;amp;amp;title=Space+Browse+virtual+tour&amp;amp;amp;border=%23ffffff%7C3px%2C1px+solid+%23999999&amp;amp;amp;output=js'&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;br /&gt;Note that Blogger amends the end of the script tag, which is OK.&lt;br /&gt;&lt;br /&gt;You can see an example of the Space Browse Google gadget running at the bottom of this web page.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-9111175086998432921?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/9111175086998432921/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=9111175086998432921' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9111175086998432921'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/9111175086998432921'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/10/google-gadget-on-your-webpage.html' title='Google gadget on your webpage'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1164349743839173166</id><published>2006-09-27T14:25:00.001+01:00</published><updated>2009-03-03T10:03:05.622Z</updated><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='widget'/><title type='text'>Snipperoo widget</title><content type='html'>The &lt;a href="http://www.snipperoo.com/" target="_blank"&gt;Snipperoo&lt;/a&gt; widget management system is in beta.  It lets you turn any code snippet into a "widget".  One or more widgets are then placed on a "panel".  Snipperoo then provides a JavaScript "codeline" for you to copy into your site, blog, etc.&lt;br /&gt;&lt;br /&gt;You can see an example Snipperoo widget - my Space Browse viewer - at the bottom of this blog post, using this codeline:&lt;br /&gt;&lt;code&gt;&amp;lt;script type='text/javascript' src='http://codelines.snipperoo.com/chriscant/snipperooney/codeline.js'&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;br /&gt;Note that blogger will not let you place this codeline within a blog post - it has to be in the template.  I have code the template so that the Snipperoo widget only shows when the specific blog post is shown, as follows:&lt;br /&gt;&lt;code&gt;&amp;lt;b:if cond='data:blog.url == "http://chriscant.phdcc.com/2006/09/snipperoo-widget.html"'&amp;gt;&lt;br /&gt;&amp;lt;/b:if&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;To make a Snipperoo widget, register for their beta program.  Make a new panel with dimensions 300x280.  Make a new module.  To make a Space Browse viewer widget, use this widget code:&lt;br /&gt;&lt;code&gt;&amp;lt;iframe src="http://www.spacebrowse.com/gadget/SpaceBrowseViewer.aspx?Caller=SN" frameborder="0" scrolling="no" style="border:0px;padding:0px;margin:0px;width:100%;height:240px;"&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/code&gt;&lt;br /&gt;Click on the (+) icon to add it to your panel.  Click on "show codeline" to get the JavaScript code to paste into your site.&lt;br /&gt;&lt;br /&gt;The above widget code can be customised as described in the &lt;a href="http://www.spacebrowse.com/help/mashups.aspx#snipperoo" target="_blank"&gt;Space Browse viewer mashup guide&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1164349743839173166?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1164349743839173166/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1164349743839173166' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1164349743839173166'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1164349743839173166'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/snipperoo-widget.html' title='Snipperoo widget'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3495111468593584691</id><published>2006-09-25T10:11:00.000+01:00</published><updated>2006-09-25T10:17:32.334+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='netvibes'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Netvibes IFRAME mini module</title><content type='html'>This post describes how to put together a &lt;a href="http://www.netvibes.com/" target="_blank"&gt;Netvibes.com&lt;/a&gt; Mini Module that shows an existing web page within an IFRAME.  My Space Browse viewer example is listed in the &lt;a href="http://eco.netvibes.com/modules/9178/space-browse-virtual-tour" target="_blank"&gt;Netvibes ecosystem gallery&lt;/a&gt;.  You can also install it by clicking here: &lt;a target="_blank" href="http://www.netvibes.com/addApiModule.php?url=http://www.spacebrowse.com/gadget/SpaceBrowseNetvibes.aspx" title="Add to Netvibes"&gt;&lt;img src="http://www.netvibes.com/img/add2netvibes.gif" width="91" height="17" alt="Add to Netvibes"/&gt;&lt;/a&gt;&amp;nbsp; (Note that this image link uses &lt;code&gt;addApiModule.php&lt;/code&gt; not &lt;code&gt;subscribe.php&lt;/code&gt;.)&lt;br /&gt;&lt;br /&gt;You can see the complete code for this module if you &lt;a href="http://www.spacebrowse.com/gadget/SpaceBrowseNetvibes.aspx" target="_blank"&gt;go here&lt;/a&gt; and then view source.  Simply viewing the page shows the Edit options.&lt;br /&gt;&lt;br /&gt;A Netvibes Mini Module is a public web page.  My technique is to keep the module web page separate from the web page that does the main work.  This lets me have one viewer page that I can share across various mashup scenarios such as gadgets, widgets or plain IFRAMEs.&lt;br /&gt;&lt;br /&gt;It is assumed that you have a web page available that is designed to display output in a small space.  This might be existing gadget/widget code or perhaps PDA output.&lt;br /&gt;&lt;br /&gt;The core of an IFRAME module is this JavaScript onload handler in the header which creates an IFRAME and adds it to the module:&lt;br /&gt;&lt;code&gt;&amp;lt;script type="text/javascript"&amp;gt;&lt;br /&gt;var url = "http://www.spacebrowse.com/gadget/SpaceBrowseViewer.aspx?Caller=NV";&lt;br /&gt;NV_ONLOAD = function()&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;var m_iframe = document.createElement("iframe");&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.scrolling = "no";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.frameBorder = "0";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.src = url;&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.width="100%";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.height="240px";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.border="0px";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.padding="0px";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.margin="0px";&lt;br /&gt;&amp;nbsp;&amp;nbsp;m_iframe.overflow="hidden";&lt;br /&gt;&amp;nbsp;&amp;nbsp;NV_CONTENT.appendChild(m_iframe);&lt;br /&gt;}&lt;br /&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;br /&gt;An IFRAME is a rectangular window on a web page that displays the contents of another URL.  The web page at this other URL only sees the (small) rectangular window it is given, and cannot interact with anything else on the page including your module code.&lt;br /&gt;&lt;br /&gt;Note that the IFRAME is given a fixed height but 100% width.  Your web page can find the actual width that you have available in &lt;code&gt;document.body.scrollWidth&lt;/code&gt;.&lt;br /&gt;&lt;br /&gt;If your page is configurable then add a form with class "configuration" to the body of your module, eg to add a search parameter:&lt;br /&gt;&lt;code&gt;&amp;lt;form class="configuration" method="post" action=""&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;lt;label&amp;gt;Search:&amp;lt;/label&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;lt;input name="Search" type="text" value="" /&amp;gt;&lt;br /&gt;&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;The user's preferences are available using the Netvibes &lt;code&gt;getValue()&lt;/code&gt; JavaScript function.  You can then add any received value to your IFRAME URL as follows:&lt;br /&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;var SearchWord = getValue("Search");&lt;br /&gt;&amp;nbsp;&amp;nbsp;if( SearchWord &amp;&amp; SearchWord!="undefined")&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;url += "&amp;Search="+SearchWord;&lt;/code&gt;&lt;br /&gt;You could also add a "Height" parameter and use it to change the IFRAME height.&lt;br /&gt;&lt;br /&gt;Your module page should also include a definition of an icon to display in the module header, eg as follows:&lt;br /&gt;&lt;code&gt;&amp;lt;link rel="icon" type="image/png" href="http://www.spacebrowse.com/gadget/SpaceBrowseNetvibesIcon.png" /&amp;gt;&lt;/code&gt;&lt;br /&gt;The icon should be 16x16.  The icon can be truncated in height so concentrate the image towards the top.&lt;br /&gt;&lt;br /&gt;If you submit your module to the Netvibes then it is a good idea to have a screenshot available.  I think that this is shown reduced to 160x120, with a link to a full size image.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3495111468593584691?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3495111468593584691/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3495111468593584691' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3495111468593584691'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3495111468593584691'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/netvibes-iframe-mini-module.html' title='Netvibes IFRAME mini module'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-2385830326485716793</id><published>2006-09-21T12:13:00.000+01:00</published><updated>2006-09-21T12:17:15.088+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='liquid'/><category scheme='http://www.blogger.com/atom/ns#' term='CSS'/><title type='text'>CSS liquid captioned image list</title><content type='html'>Despite being an aged programmer (I used punched cards while at school!), I find it difficult to get to grips with XHTML page layout using CSS.  I find it very hard to get the effect that I want to achieve.  It was generally a lot easier when using tables.  Perhaps we need a new tag or attribute that works like a table but is ignored by accessibility browsers.&lt;br /&gt;&lt;br /&gt;Anyway, I like (a) liquid flows and (b) specifying fonts without using fixed point sizes.&lt;br /&gt;&lt;br /&gt;My latest task was to show a lot images.  I wanted them to fill all the available screen space, so a liquid layout was called for.  If I simply used a series of IMG tags then this would work immediately.  However I also wanted to show a caption underneath (and make the image a link).&lt;br /&gt;&lt;br /&gt;My own attempts to make this work failed.  I nearly resorted to using a non-liquid table layout.  Eventually a search found &lt;a href="http://www.alistapart.com/stories/practicalcss/" target="_blank"&gt;Mark Newhouse's guide&lt;/a&gt; (which is one of those annoying sites which come up with a smaller font than normal).  The basic idea is to have each photo/caption block in a DIV that floats to the left.  If you add lots of these then each DIV goes to the right of the previous one if there is room, and onto the next line if not.  You need to round it off with a DIV style clear:both.  Thanks Mark.&lt;br /&gt;&lt;br /&gt;My eventual solution can be seen &lt;a href="http://www.spacebrowse.com/sites/viv/35/Default.aspx?View=all" target="_blank"&gt;here&lt;/a&gt;.  Even this solution has its problems, stemming from the fact that the caption text is of variable length.  If I set a fixed style height then FF and O would truncate the text, even with overflow:visible set.  So I guessed a suitable min-height.  The problem remains that picture/caption blocks have different heights.  So when a block floats down, it may display at the block that sticks down the most - which is not necessarily at the start of the line.  You can see this after a row or three using the above link.&lt;br /&gt;&lt;br /&gt;There are also printing problems.  IE only shows one page of floating DIVs before giving up.  FF does display all the DIVs but those at the bottom of the page are often not shown in their entirety.&lt;br /&gt;&lt;br /&gt;Any better suggestions appreciated.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-2385830326485716793?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/2385830326485716793/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=2385830326485716793' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2385830326485716793'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/2385830326485716793'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/css-liquid-captioned-image-list.html' title='CSS liquid captioned image list'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-3711331348777150828</id><published>2006-09-15T11:03:00.000+01:00</published><updated>2006-09-15T11:19:26.658+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Visual Studio 2005'/><category scheme='http://www.blogger.com/atom/ns#' term='Find-in-files'/><title type='text'>Find-in-Files for ascx.cs in Visual Studio</title><content type='html'>Just a quick warning to say that Visual Studio 2005 (and probably 2003) Find-in-Files will not by default find text in *.aspx.cs files if you use the standard file types spec: &lt;code&gt;*cs;*.vb;*.resx;*.xsd;*.wsdl;*.htm;*.html;*.aspx;*.ascx;&lt;br /&gt;*.asmx;*.asax;*.config;*.asp;*.asa;*.css;*.xml&lt;/code&gt; even though &lt;code&gt;*cs&lt;/code&gt; is in the spec.&lt;br /&gt;&lt;br /&gt;I reported this here in &lt;a href="http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=144168" target="_blank"&gt;Microsoft Connect Feedback&lt;/a&gt; but Microsoft insist that it is by design.  Weird!&lt;br /&gt;&lt;br /&gt;Anyway, make sure that you add &lt;code&gt;*.aspx.cs&lt;/code&gt; and other variants (eg &lt;code&gt;*.asmx.cs&lt;/code&gt;) to your file spec, separated by semi-colons.  I also add &lt;code&gt;*.js;*.master&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Actually, I've just noticed that the standard file spec has &lt;code&gt;*cs&lt;/code&gt;.  If you change this to &lt;code&gt;*.cs&lt;/code&gt; with a period then it also works.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-3711331348777150828?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/3711331348777150828/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=3711331348777150828' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3711331348777150828'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/3711331348777150828'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/find-in-files-for-ascxcs-in-visual.html' title='Find-in-Files for ascx.cs in Visual Studio'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-1732516612755554494</id><published>2006-09-12T12:27:00.000+01:00</published><updated>2006-09-12T12:44:27.437+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='tag'/><category scheme='http://www.blogger.com/atom/ns#' term='tag-aware'/><category scheme='http://www.blogger.com/atom/ns#' term='widget'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><category scheme='http://www.blogger.com/atom/ns#' term='widgetbox'/><title type='text'>Tag aware widget</title><content type='html'>This posts shows an example of a &lt;a href="http://www.widgetbox.com" target="_blank"&gt;widgetbox&lt;/a&gt; tag aware widget - using my &lt;a href="http://widgetbox.com/details_view.jsp?widget=268cd5e9-6527-486b-94d1-c8447112af5d" target="_blank"&gt;Space Browse virtual tour widget&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;This blog entry contains a standard widgetbox DIV to place the widget and an extra A tag to define the tag.  The tag is "hoff" which tells Space Browse to search for a virtual tour that contains the word "hoff".  Here's the HTML:&lt;br /&gt;&lt;code&gt;&amp;lt;div class="wbx-widget" id="f3a39c0a-cbf9-4c51-92fa-d1cf6abfe54f"&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&amp;lt;a rel="wbx-keywords" title="hoff"&amp;gt;&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;You should always see the virtual tour called "Rock climbing at The Hoff" here.&lt;br /&gt;&lt;br /&gt;&lt;div class="wbx-widget" id="f3a39c0a-cbf9-4c51-92fa-d1cf6abfe54f"&gt;&lt;/div&gt;&lt;a rel="wbx-keywords" title="hoff"&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Here's the widgetbox docs for tags:&lt;ul&gt;&lt;li&gt;&lt;a href="http://www.widgetbox.com/docs/smartblogs/" target="_blank"&gt;How to use in a blog&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="http://forums.widgetbox.com/viewtopic.php?pid=18#p18" target="_blank"&gt;How to configure a subscription widget&lt;/a&gt;&lt;/li&gt;&lt;li&gt;If you register your own widget, in the "Add Configuration Parameters" page, set up a suitable parameter then click "Edit Options" then check the "Tag Aware" checkbox and click "Apply Changes".&lt;/li&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-1732516612755554494?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/1732516612755554494/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=1732516612755554494' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1732516612755554494'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/1732516612755554494'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/tag-aware-widget.html' title='Tag aware widget'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-7971397995355484592</id><published>2006-09-08T08:52:00.000+01:00</published><updated>2006-09-08T09:11:30.204+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='accessibility'/><category scheme='http://www.blogger.com/atom/ns#' term='Internet Explorer'/><category scheme='http://www.blogger.com/atom/ns#' term='tabindex'/><title type='text'>Tabindex order accessibility issues</title><content type='html'>I released a new version of the &lt;a href="http://www.spacebrowse.com/" target="_blank"&gt;Space Browse&lt;/a&gt; viewer yesterday.  This was a major update because it made the viewer more accessible to keyboard users.  However there are some unresolved issues... so any help appreciated.&lt;br /&gt;&lt;br /&gt;The viewer has various tool button images used for navigation etc, as can be seen on the right in the "Space Browse widget".  The previous viewer version used DIV HTML elements with onclick handlers added.  The new version uses INPUT TYPE="image" elements (with onclick handlers) that let users use the tab key to move between focus elements on the page, thereby hopefully improving accessibility.&lt;br /&gt;&lt;br /&gt;The new viewer INPUT elements all have TABINDEX attributes to set the order of the controls.  For the viewer I started the TABINDEX values at 200.  This helps the overall page layout:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Master page header: tabindex values start at 1&lt;/li&gt;&lt;li&gt;Page before viewer: tabindex values start at 100&lt;/li&gt;&lt;li&gt;Viewer: tabindex values start at 200&lt;/li&gt;&lt;li&gt;Page after header: tabindex values start at 300&lt;/li&gt;&lt;li&gt;Master page footer: tabindex values start at 400&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;I had to be careful with the INPUT image element borders.  Originally I had &lt;code&gt;border:1px solid gray;&lt;/code&gt;.  However this did not work well in IE because it shows the element with the focus by giving it a gray dashed 1px border - this did not show up.  So I used other colours.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Issue 1: tab order of elements added in JavaScript&lt;/strong&gt;&lt;br /&gt;My new viewer code adds some INPUT elements in JavaScript after the static elements have loaded.  These added INPUT elements do have TABINDEX values in the correct order.  However browsers do not respect these values - instead the elements are treated as if they had no TABINDEX, ie the user tabs to them after all the items that do have a TABINDEX.  Tested in IE6, FF1.5 and O9 in Windows XP.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Issue 2: IE added elements invocation&lt;/strong&gt;&lt;br /&gt;Once you have tabbed to aan element in Internet Explorer you can press either Space or Enter to "click" it.  When I added INPUT elements in JavaScript I saw a weird bug.  If you use Space then the correct element is "clicked".  However, if you use Enter then the wrong element is "clicked", ie the first added element is always "clicked".  Tested in IE6 in Windows XP.&lt;br /&gt;&lt;br /&gt;This is the code that I use:&lt;br /&gt;&lt;code&gt;fInput["onclick"] = function() { return space_gotoFrame(this.alt,true,null); }&lt;/code&gt;&lt;br /&gt;where fInput is the INPUT element.  When clicked, this.alt should be the filename of the photo to go to.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Issue 3: Opera tabbing&lt;/strong&gt;&lt;br /&gt;If appropriate, the viewer removes various INPUT elements by setting &lt;code&gt;display:none;&lt;/code&gt;.  In Opera 9 in Windows XP this caused problems with the tab order: Opera does not skip such elements, instead it goes back to the start of the page and so does not let you tab to all page elements.&lt;br /&gt;&lt;br /&gt;Regards other browsers: FireFox was best at tabbing because the focus element stayed the same after a "click" even if the element was disabled.  Interner Explorer always loses the focus element if it is disabled.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Later: ASP.NET asp:TreeView TabIndex&lt;/strong&gt;&lt;br /&gt;The ASP.NET asp:TreeView server control has a TabIndex property.  However it does not implement it very well.  The TreeView is implemented as a DIV with tables inside.  The DIV is given the specified TabIndex; however the links inside are not, so those links appear at the wrong place in the tab order&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-7971397995355484592?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/7971397995355484592/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=7971397995355484592' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7971397995355484592'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/7971397995355484592'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/tabindex-order-accessibility-issues.html' title='Tabindex order accessibility issues'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-5549562876594997744</id><published>2006-09-07T10:03:00.000+01:00</published><updated>2006-09-07T10:18:11.783+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='photo'/><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='Google'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Space Browse viewer Google gadget</title><content type='html'>The Space Browse virtual tour viewer Google gadget is now listed in the &lt;a href="http://www.google.com/ig/directory?url=http://www.spacebrowse.com/gadget/SpaceBrowse.xml" target="_blank"&gt;Google content directory&lt;/a&gt;.  Click on the "Add it now" button to add it to your personalised home page.  As yet, I haven't been able to find out which directory category it has been put in.&lt;br /&gt;&lt;br /&gt;I spotted the directory entry after I'd signed up for a Google Pages account.  The Space Browse gadget works fine there, as can be seen here:  &lt;a href="http://jchriscant.googlepages.com" target="_blank"&gt;jchriscant.googlepages.com&lt;/a&gt;.  To add to your Pages site you must have the Experimental features enabled.  Then edit a page, click in an editable field then press "More".  With "Standard gadgets" selected, search for "space browse". Click on "Space Browse virtual tour", then "Add Gadget &gt;&gt;" and agree "OK".&lt;br /&gt;&lt;br /&gt;I have improved the Space Browse gadget since &lt;a href="http://chriscant.blogspot.com/2006/08/made-first-google-gadget-for-space.html"&gt;my previous post&lt;/a&gt;.  It now fits into the given space better and has various parameters.  I noticed that you can resize the rectangle for the gadget in Google Pages, so I will have to add in parameters to let you set the display size.&lt;br /&gt;&lt;br /&gt;As an aside, I have been working on version 2 of the viewer/gadget/widget.  This uses proper HTML INPUT fields to show the navigation controls.  This makes the viewer more accessible to keyboard users.  When this is done I will be able to submit it to the Windows Live gallery.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-5549562876594997744?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/5549562876594997744/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=5549562876594997744' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5549562876594997744'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/5549562876594997744'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/09/space-browse-viewer-google-gadget.html' title='Space Browse viewer Google gadget'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115654055179104879</id><published>2006-08-25T21:50:00.000+01:00</published><updated>2006-08-30T16:41:22.716+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='photo'/><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='live spaces'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='live.com'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Another day, another gadget</title><content type='html'>It seems like there are quite a few sorts of mash up gadgets and widgets to be had: Microsoft gadgets, Yahoo widgets and Apple dashboard widgets to name but a few.&lt;br /&gt;&lt;br /&gt;Today I put together a Space Browser virtual tour viewer Microsoft Gadget, based on the LazyGadget example.  So far it doesn't have any parameters and has not been added to the Microsoft Live Gallery yet.  [Later: &lt;a href="http://gallery.live.com/LiveItemDetail.aspx?li=832c1038-2f5c-41aa-840d-b90976020324" target="_blank"&gt;here it is in the gallery&lt;/a&gt; - go to this page to "Add to Live.com" or "Add to your space".&lt;br /&gt;&lt;a href="http://photos1.blogger.com/blogger2/2104/3960/1600/MS_OK_to_Add_to_Live.gif"&gt;&lt;img style="cursor:pointer; cursor:hand;" src="http://photos1.blogger.com/blogger2/2104/3960/320/MS_OK_to_Add_to_Live.png" border="0" alt="" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Or you can add it your Windows Live site at &lt;a href="http://www.live.com" target=_blank&gt;www.live.com&lt;/a&gt; by clicking on Add Stuff then Advanced Options.  After "Add a Gadget by URL", enter the gadget manifest XML URL:&lt;br /&gt;&lt;a href="http://www.spacebrowse.com/gadget/gadget.xml" target=_blank&gt;http://www.spacebrowse.com/gadget/gadget.xml&lt;/a&gt; and click on "Subscribe".  The site then prompts "Only install Gadgets from trusted sites".  Click on "Install Gadget" after selecting "Remember my preference".&lt;br /&gt;&lt;a href="http://photos1.blogger.com/blogger/7693/3546/1600/MS_OK_to_install_Gadget.gif"&gt;&lt;img src="http://photos1.blogger.com/blogger/7693/3546/320/MS_OK_to_install_Gadget.png" border="1" alt="Only install Gadgets from trusted sites" /&gt;&lt;/a&gt;&lt;br /&gt;That should be it.  You should be able to move or collapse the gadget as normal.  If you close your browser while the gadget is collapsed then it will not display correctly when expanded unless you refresh the page.&lt;br /&gt;&lt;br /&gt;You can also add the gadget to your Windows Live Spaces site - click on "Add to your space" in the &lt;a href="http://gallery.live.com/LiveItemDetail.aspx?li=832c1038-2f5c-41aa-840d-b90976020324" target="_blank"&gt;Gallery listing&lt;/a&gt;&lt;br /&gt;&lt;span style="text-decoration:line-through;"&gt;Currently the gadget will only be visible to you when logged in; visitors will not see it.  To add the gadget, click on this link:  &lt;a href="http://spaces.live.com/spacesapi.aspx?wx_action=create&amp;wx_url=http://www.spacebrowse.com/gadget/gadget.xml" target=_blank&gt;http://spaces.live.com/spacesapi.aspx?wx_action=create&amp;wx_url=http://www.spacebrowse.com/gadget/gadget.xml&lt;/a&gt;.  Again, you will have to confirm the installation.&lt;br /&gt;&lt;br /&gt;When the gadget is listed in the Windows Live Gallery you will be able to add it and make it visible to all viewers.  &lt;/span&gt;&lt;br /&gt;It is present on my Live Space at &lt;a href="http://chriscant.spaces.live.com" target=_blank&gt;chriscant.spaces.live.com&lt;/a&gt; &lt;span style="text-decoration:line-through;"&gt;but you will not be able to see it as a visitor.&lt;/span&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115654055179104879?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115654055179104879/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115654055179104879' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115654055179104879'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115654055179104879'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/another-day-another-gadget.html' title='Another day, another gadget'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115633908732818151</id><published>2006-08-23T14:07:00.000+01:00</published><updated>2006-08-24T12:23:46.363+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='photo'/><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='widget'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><category scheme='http://www.blogger.com/atom/ns#' term='widgetbox'/><title type='text'>My gadget's now also a widget</title><content type='html'>I've improved my Space Browse Google gadget since the previous post so that it fills the screen area better and has optional parameters to choose random or recent tours and tours just by a specified username.&amp;nbsp;  Having provided a pithy author_aboutme and author_quote etc, I will submit it for listing in the Google content directory.&amp;nbsp;  My gadget also seems to work OK on the Google Desktop Sidebar.&lt;br /&gt;&lt;br /&gt;I had previously signed up to &lt;a href="http://widgetbox.com/" target=_blank&gt;Widgetbox&lt;/a&gt;.&amp;nbsp;  Last night I was able to add my gadget as a widget very easily - it worked first time, hurray!&amp;nbsp;  Here it is: &lt;a href="http://widgetbox.com/details_view.jsp?widget=268cd5e9-6527-486b-94d1-c8447112af5d" target=_blank&gt;Widgetbox Space Browse widget&lt;/a&gt;.&amp;nbsp; Today I updated it to have parameters.&amp;nbsp;  Where the gadget parameter was &lt;code&gt;ShowMode&lt;/code&gt;, I specified a widget parameter called &lt;code&gt;up_ShowMode&lt;/code&gt;, etc.&lt;br /&gt;&lt;br /&gt;Widgetbox lets you add a widget directly onto any web page, eg onto a blogger template as can be seen below - optionally, in an widgetbox panel.&amp;nbsp;  &lt;span style="text-decoration:line-through;"&gt;In Firefox, the widget overlaps the dotted vertical divider, which doesn't happen in Internet Explorer 6.&lt;/span&gt;&amp;nbsp;  To add a widget to a blogger blog post you have to add a widgetbox panel (empty if you want) in the template and then use a specified DIV within the post itself:&lt;br /&gt;&lt;br /&gt;&lt;div class="wbx-widget" id="f3a39c0a-cbf9-4c51-92fa-d1cf6abfe54f"&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115633908732818151?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115633908732818151/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115633908732818151' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115633908732818151'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115633908732818151'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/my-gadgets-now-also-widget.html' title='My gadget&apos;s now also a widget'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115615468079277118</id><published>2006-08-21T10:52:00.000+01:00</published><updated>2006-08-21T11:04:40.803+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Java'/><category scheme='http://www.blogger.com/atom/ns#' term='applet'/><category scheme='http://www.blogger.com/atom/ns#' term='Internet Explorer'/><category scheme='http://www.blogger.com/atom/ns#' term='IE7'/><title type='text'>Internet Explorer 7: Locally run Java applet getAppletContext().showDocument(url) doesn't work - drive repeated</title><content type='html'>This issue has been fixed in Internet Explorer 7 RC1.  Great!&lt;br /&gt;[I can only assume that Microsoft did not acknowledge the problem in my bug report because they were testing with the fixed version rather than the beta 3 version.]&lt;br /&gt;&lt;br /&gt;&lt;span style="text-decoration:line-through;"&gt;&lt;br /&gt;Just before last week's break, I posted feedback about Internet Explorer 7 beta 3.&lt;br /&gt;&lt;br /&gt;A customer of our &lt;a href="http://www.phdcc.com/fiscd/"&gt;FindinSite-CD&lt;/a&gt; software had reported that a user of theirs had tried our software in IE7.  It ran into a problem when run locally, ie from CD.&lt;br /&gt;&lt;br /&gt;The problem is that a Local Machine Java applet call to getAppletContext().showDocument(url) does not work for a relative URL because the drive portion of the file URL is repeated.  Instead, an error such as this is shown in a message box: &lt;code&gt;Cannot find file 'file:///C:/C:/PHD/IE7test/iamshown.htm'&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Here's &lt;a href="http://connect.microsoft.com/IE/feedback/ViewFeedback.aspx?FeedbackID=179615"&gt;my feedback report to Microsoft&lt;/a&gt; - you will need to register to see it.  It seems as though Microsoft were unable to reproduce the problem and so closed the issue.  If you can repeat it, please Validate and Vote for it once you have confirmed that my posted example fails as described.&lt;br /&gt;&lt;br /&gt;Assuming that this is a real problem, then it will make our software not work in IE7 which is a crucial browser for us.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;PS  Just saw an article in Ethical Consumer which could be summarised as "New organic range of underwear approved by Soil Association".&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115615468079277118?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115615468079277118/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115615468079277118' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115615468079277118'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115615468079277118'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/internet-explorer-7-locally-run-java.html' title='Internet Explorer 7: Locally run Java applet getAppletContext().showDocument(url) doesn&apos;t work - drive repeated'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115524800174359914</id><published>2006-08-10T22:59:00.000+01:00</published><updated>2006-08-21T15:00:47.423+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='photo'/><category scheme='http://www.blogger.com/atom/ns#' term='virtual tour'/><category scheme='http://www.blogger.com/atom/ns#' term='gadget'/><category scheme='http://www.blogger.com/atom/ns#' term='Google'/><category scheme='http://www.blogger.com/atom/ns#' term='mashup'/><title type='text'>Made a first Google Gadget for Space Browse</title><content type='html'>Just a brief note to say that I managed to make a Google gadget very quickly for my Space Browse virtual tour site.  Having read quickly through the Google Gadget starter documentation earlier in the day, it only took an hour or so to make a gadget that shows a random virtual tour.&lt;br /&gt;&lt;br /&gt;I had to make a new web form page that used a new stored procedure to get a random publicly accessible photo-site.  Thanks to whoever it was who had the tip that adding ORDER BY NEWID() gives a random result set for a SELECT statement.  This works by generating a new uniqueidentifier/guid for each result row; because the Guids are random, ordering on this will return a randomly sorted result set.  I added TOP 1 because I only wanted one random result.&lt;br /&gt;&lt;br /&gt;The gadget found a bug in my existing code that assumed that something divided by something else was an integer.  I fixed it by using Math.floor().&lt;br /&gt;&lt;br /&gt;The gadget is still basic because it does not respond to the provided width.  It also needs to remove some of its borders.&lt;br /&gt;&lt;br /&gt;Anyway, here's the gadget:&lt;br /&gt;&lt;a href="http://www.spacebrowse.com/gadget/SpaceBrowse.xml"&gt;http://www.spacebrowse.com/gadget/SpaceBrowse.xml&lt;/a&gt; &lt;a href="http://fusion.google.com/add?moduleurl=http%3A//www.spacebrowse.com/gadget/SpaceBrowse.xml"&gt;&lt;br /&gt;  &lt;img src="http://buttons.googlesyndication.com/fusion/add.gif" width="104" height="17" border="0" alt="Add to Google" /&gt;&lt;br /&gt; &lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115524800174359914?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115524800174359914/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115524800174359914' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115524800174359914'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115524800174359914'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/made-first-google-gadget-for-space.html' title='Made a first Google Gadget for Space Browse'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115513276264754863</id><published>2006-08-09T15:10:00.000+01:00</published><updated>2006-08-09T15:25:12.033+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='asp:HyperLink'/><category scheme='http://www.blogger.com/atom/ns#' term='ASP.NET'/><title type='text'>asp:HyperLink includes an extra space in the link</title><content type='html'>Only a minor point, but poor form...&lt;br /&gt;If you have an asp:HyperLink in an ASP.NET2 app then it is sometimes rendered with an extra space within the link which gives an underlined space at the end of the link.&lt;br /&gt;&lt;br /&gt;Suppose you have the following:&lt;br /&gt;&lt;code&gt;&amp;lt;asp:HyperLink ID="idLink" runat="server" Text="Hello" /&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;When rendered incorrectly, there are line breaks between these three lines which causes the problem:&lt;br /&gt;&lt;code&gt;&amp;lt;a id="ctl00_MasterLoginView1_something" href="something.aspx"&amp;gt;&lt;br /&gt;Hello&lt;br /&gt;&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;When rendered correctly, there are no line breaks.&lt;br /&gt;&lt;br /&gt;I haven't been able to determine exact circumstances when rendering is incorrect.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115513276264754863?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115513276264754863/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115513276264754863' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115513276264754863'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115513276264754863'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/asphyperlink-includes-extra-space-in.html' title='asp:HyperLink includes an extra space in the link'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-32442424.post-115511539163149529</id><published>2006-08-09T09:52:00.000+01:00</published><updated>2006-08-10T14:32:55.163+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='cookies'/><category scheme='http://www.blogger.com/atom/ns#' term='ASP.NET'/><category scheme='http://www.blogger.com/atom/ns#' term='cookieless'/><category scheme='http://www.blogger.com/atom/ns#' term='Forms authentication'/><title type='text'>Coping with links in cookieless mode in ASP.NET2</title><content type='html'>A new web site project that I am working on might be used a lot from internet cafes and the like where it is possible that cookies are disabled. I therefore started to look at how to support cookieless mode in an existing alpha web site of mine &lt;a href="http://www.spacebrowse.com"&gt;www.spacebrowse.com&lt;/a&gt; - go there to try it out with cookies disabled once you have registered for free.&lt;br /&gt;&lt;br /&gt;The ASP.NET2 site uses Forms authentication, specified in Web.Config as follows:&lt;br /&gt;&lt;code&gt;&amp;lt;authentication mode="Forms"&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;forms protection="All"&amp;gt;&lt;br /&gt;&amp;lt;/authentication&amp;gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;cookieless - UseDeviceProfile&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;The forms element "cookieless" attribute has a default value of "UseDeviceProfile". Basically this doesn't work if a browser has cookies turned off. "UseDeviceProfile" looks at the type of viewer, so it will assume that cookies are turned on for IE/FF/etc, but will assume they are not for some other device types.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;cookieless - UseUri&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Another option for "cookieless" is "UseUri" which means that cookies are not used. Instead, ASP.NET2 mangles the URL to include the information that would have been stored in the cookie. A http: URL such as this:&lt;br /&gt;&lt;code&gt;http://localhost/SpaceBrowse/Default.aspx&lt;/code&gt;&lt;br /&gt;is mangled to look like this:&lt;br /&gt;&lt;code&gt;http://localhost/SpaceBrowse/(F(ekocDSRz...PZKv3ah41))/Default.aspx&lt;/code&gt;&lt;br /&gt;where a lot of characters have been removed at the ellipses.&lt;br /&gt;&lt;br /&gt;If you look carefully, you will see that the mangled URL contains an extra folder name. When I saw this, I thought that all resources and relative links from this page would go wrong. In fact, they don't because the ASP.NET ISAPI DLL filter intercepts all requests and resolves them correctly if it detects mangled URLs. Also note that Request.Path as seen by the ASPX page is correctly SpaceBrowse/Default.aspx, ie without any mangling.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;cookieless mangled URL links&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;However there is an issue as regards links. If you want the user to stay logged in, then the URL they browse to has to have a mangled URL. This means that you must use relative links to stay logged in. Any absolute links will not include the mangled URL. You will be OK if you use &lt;code&gt;&amp;lt;asp:hyperlink&amp;gt;&lt;/code&gt; and the like with &lt;code&gt;NavigateUrl="~/sites.aspx"&lt;/code&gt; or whatever because ASP.NET resolves the ~/ correctly.&lt;br /&gt;&lt;br /&gt;My MasterPage template is used by pages in various directories. My original code included static absolute HTML links to pages on the site. These links did not include the mangled URL and so the user's login status is forgotten. The solution is to use ASP.NET controls for all such links (even though that might require a small amount more server-side processing).&lt;br /&gt;&lt;br /&gt;Aside 1:  When testing "UseUri" mode in IE using ASP.NET Development Server, if you have links to directories then they will not work if they end in "/".  You must change them to "/Default.aspx" to be able to see the requested page in cookieless mode.  When working through IIS, ordinary directory requests will work OK.&lt;br /&gt;&lt;br /&gt;Actually, it is important that &lt;b&gt;all your links to directories end in /&lt;/b&gt; - otherwise the mangled URL will be lost.&lt;br /&gt;&lt;br /&gt;Aside 2:  When you log off in mangled URL mode (using asp:LoginStatus LogoutPageUrl="~/default.aspx"), the URL you get redirected looks like this:&lt;br /&gt;&lt;code&gt;http://localhost/SpaceBrowse/(X(1))/default.aspx&lt;/code&gt;&lt;br /&gt;Although strange, everything works OK.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;cookieless - AutoDetect&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;Anyway, back at my original problem: how to cope if a browser has cookies turned off.  The best option is to set "cookieless" to "AutoDetect" in the Forms element.  With this setting, ASP.NET probes to see if cookies are set; if cookies are enabled, then they are used; if not, then mangled URLs are used.&lt;br /&gt;&lt;br /&gt;The probing mechanism seems to kick in when you click a Login button.  To determine if a browser session has cookies enabled, this parameter is added to the login URL:  &lt;code&gt;AspxAutoDetectCookieSupport=1&lt;/code&gt;.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;PS&lt;/strong&gt;. I found it easiest to test in FF rather than IE because FF has a simple method of turning off cookies; IE does it on a zone basis which I could quickly not get to work for the dev server URL &lt;code&gt;http://localhost:1234/whatever/&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;PPS.&lt;/strong&gt; Found that cookies must be enabled for yahoo/flickr to let you log on.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/32442424-115511539163149529?l=chriscant.phdcc.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://chriscant.phdcc.com/feeds/115511539163149529/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=32442424&amp;postID=115511539163149529' title='7 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115511539163149529'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/32442424/posts/default/115511539163149529'/><link rel='alternate' type='text/html' href='http://chriscant.phdcc.com/2006/08/coping-with-links-in-cookieless-mode_09.html' title='Coping with links in cookieless mode in ASP.NET2'/><author><name>Chris Cant</name><uri>http://www.blogger.com/profile/11367082039820244178</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='29' height='32' src='http://bp0.blogger.com/_hkkBsHR0r5s/SIidQwvn23I/AAAAAAAAABA/1Xq-6vG4S7g/S220/chris19.jpg'/></author><thr:total>7</thr:total></entry></feed>
