Thursday, September 30, 2004
I was catching up on some blog reading and ran across this link in Eric Gunnerson's C# Compendium.  Great game that tests your ability to not follow your instincts.  Fantastic game!
Thursday, September 30, 2004 4:07:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback

If you're a website developer you may have run across several of the nuances presented with the more secure IE 6 for Windows XP SP2 - namely the Information Bar (the nice yellow bar that appears at the top of the webpage when the page attempts to perform some restricted action such as display a popup the user didn't request, or the unsolicited installation of an ActiveX control).

Here are a couple of great links to follow to learn more about those restrictions and how to make your website play friendly in their new environment:

Compatibility in Internet Explorer 6 for Windows XP Service Pack 2

Fine-Tune Your Web Site for Windows XP Service Pack 2

Thursday, September 30, 2004 3:50:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, September 29, 2004

Last night I was greeted to a very unpleasant, and unsettling occurrence that had me baffled until today when I got some help from a great friend - one of my old co-worker buddies from Microsoft.  My network was up and everything was fine (internally), but I couldn't get any DNS resolution outside my network (i.e. out on the internet).

As it turns out I could ping my router from every computer on the network except my domain controller (which is also my DNS server).  Because my DNS server couldn't resolve anything, I was pretty dead in the water.  I tried several things to fix the issue, but nothing worked.  It was suggested by my buddy to change the server's IP address and see if I couldn't still ping the router.  Lo and behold that was the problem!

Somehow, somewhere, the router was denying traffic from my old IP address.  Without any concrete evidence, I attribute this problem to a pretty bad power outage that we had a few days ago.  Even after powering down the router for several minutes and starting it back up I could not get connectivity on the old IP so I had to resort to a new IP.  Crazy, but at least it works!

Ya know? I've been in the contemplating getting a new, good, reliable router anyway...maybe this is the incentive that I need.  This one that I have (a NetGear) has been fantastic over the past several years, but I've been wanting to get something newer and better; budget is not the primary concern - but practicality is.

Any suggestions?

Wednesday, September 29, 2004 3:24:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [3]  |  Trackback
 Monday, September 27, 2004

Today I had an issue in which I wanted to build out a tree of data.  This information is collected in a single table with self joins to define the hierarchy.  I wanted to avoid having to repeatedly query the database for a subset of the records - instead, I wanted to get the whole batch from the database, already organized and set up in the tree.  I've done things such as this in Oracle via the CONNECT BY statement in the past, but unfortunately SQL Server (in its current incarnation) doesn't support this type of functionality; therefore, I had to resort to rolling my own.

A join (either OUTER or INNER) did not give me what I wanted, though the relationship could be established, so I decided to write a stored procedure to accomplish this.  Now, to be honest, I have not profiled this to see how it can be optimized (I'd welcome insights regarding that) but for small set of data (e.g. < 10,000 rows) I think this approach might work reasonably well.

Basically, the idea is that the stored procedure establishes two temporary tables: one for the final output and one for the work in progress.  You can call the procedure, indicating the root node for the hierarchy (defaulting to the top - those items that have no parent; identified by NULL).  From that point, the procedure begins to analyze the data, writing into the temporary work table (called #Stack) and recording records as necessary into the #Output table.

My strategy (as this will be used in a web-based application and will rarely, if ever, change) is to query it once and cache it so I don't have to perform this analysis over and over again.  Additionally, I'm really only expecting < 50 records so the hit is pretty minimal.  What do you think of the approach?  Good?  Bad?  Ugly?  Ingenious?  Mediocre?  I'd be interested in getting some good feedback.

CREATE PROCEDURE dproc_GetCategoryList
   @RootParentID int = NULL
AS
   SET NOCOUNT ON

   -- establish the table that will contain the output results
   CREATE TABLE #Output ( ID int, Name nvarchar(35), ParentID int )

   -- establish the work table
   -- this data will fluctuate and will represent the information that is being processed
   CREATE TABLE #Stack ( ID int, Depth int )

   DECLARE @currID int
   DECLARE @depth int

   IF ( @RootParentID IS NULL )
      -- start at the parent level (there might be multiple top-level parents) and start working our way down
      -- top-level parents are identified by not having parent themselves
      DECLARE topLevel CURSOR FOR
      SELECT ID FROM tblCategoryIndex WHERE ParentID IS NULL ORDER BY Name
   ELSE
      -- start at the level prescribed by the caller and build out the child hierarchy
      DECLARE topLevel CURSOR FOR
      SELECT ID FROM tblCategoryIndex WHERE ID = @RootParentID

   OPEN topLevel
   FETCH NEXT FROM topLevel INTO @currID
      WHILE ( @@FETCH_STATUS = 0 )
         BEGIN
         SET @depth = 1
         INSERT INTO #Stack VALUES ( @currID, @depth )

         WHILE ( @depth > 0 )
            BEGIN
               IF EXISTS ( SELECT ID FROM #Stack WHERE Depth = @depth )
                  BEGIN
                     -- read the next record's id for processing so we can get its children and delete it
                     SELECT @currID = ID FROM #Stack WHERE Depth = @depth
                     DELETE FROM #Stack WHERE Depth = @depth AND ID = @currID

                     -- add the category to the output table
                     INSERT INTO #Output SELECT ID, Name, ParentID FROM tblCategoryIndex WHERE ID = @currID

                     -- get the children of the current category
                     INSERT INTO #Stack SELECT ID, @Depth + 1 FROM tblCategoryIndex WHERE ParentID = @currID ORDER BY Name DESC

                     IF ( @@ROWCOUNT > 0 )
                        SET @depth = @depth + 1
                  END
               ELSE
                  -- move up one level, we're recursed as deep on this node branch as we can go
                  SET @depth = @depth - 1
            END

         FETCH NEXT FROM topLevel INTO @currID
      END
   CLOSE topLevel
   DEALLOCATE topLevel

   SET NOCOUNT OFF

   -- return the result set
   SELECT * FROM #Output
GO

- [Updated 09/28/2004 for proper sorting of the name - the list is now properly alphabetized in the result set.]

Monday, September 27, 2004 8:40:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Friday, September 24, 2004

For all those programmers out there that think that it's important to write code that is easily maintainable, think again!  You want job security, right?  Think about yourselves for a change - be selfish!  It is of utmost importance to create code that is not only unmaintainable by the unsuspecting inheritor of your code, but if you have to spin your wheels going back over something you just wrote - all the better - it'll keep you employed and your boss will think that you're worth every penny by solving these insurmountable problems found in the mazes and jungles of code!

For more details, check this site out.

Friday, September 24, 2004 11:20:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Thursday, September 16, 2004

I've been debugging this issue (or rather, attempting to debug this issue) wherein I have some ASP.NET controls that perform postbacks.  These controls are of my own make and are UserControls (.ascx).  Ages ago I thought I'd be clever and set the the EnableViewState property of the page to false because the information I'm presenting rarely (if ever) changes.  I query it once and cache the data and redisplay it on round trips to the server - not a big deal.

...until I decide to put a DataList on the control and want to capture the ItemCommand event on items within the list.  It turns out that unless the Page has EnableViewState set to true, the control events don't get wired up properly.  Now I've seen this behavior before and I've known about these circumstances from eons ago, but it's funny the little things you forget in an attempt to be clever.

Thursday, September 16, 2004 7:56:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, September 14, 2004
This is my first blog entry from within InfoPath.  Upon reading Aaron Skonnard's blog entry today I couldn't help but jump on the bandwagon.  I too have been bitten on more than one occasion by the lack of XML support in the .Text admin section or by an expiring session while entering a new blog entry.  This is a welcome change.
 
Kudos to Quoqiang Wu for his most excellent InfoPath SimpleBlogger!
Tuesday, September 14, 2004 6:11:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback

Here's a code snippet that I wrote the other day.  I had a need to discover the various SQL Server instances on the network.  I couldn't reliably use SQL DMO for a variety of reasons; for one, there might not be any client tools installed locally.  Therefore, I decided to dive down into ODBC and use its built-in functionality that I am pretty confident will be on the users' machines.

There are a few items I'd like to state before we get to the code.

  • This code will not work (that is, it will return null) if you don't currently have an active network connection
  • I'm working on getting this to work regardless of connectivity - so stay tuned.
  • Similar functionality is expected in VS 2005, but this gets the job done.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;

namespace Devstone.Tools.DataAccess.Discovery {

   public sealed class DbEnumerator {
      [DllImport("odbc32.dll")]
      private static extern short SQLAllocHandle(short hType, IntPtr inputHandle, out IntPtr outputHandlePtr);
      [DllImport("odbc32.dll")]
      private static extern short SQLSetEnvAttr(IntPtr hEnv, int attribute, IntPtr valuePtr, int stringLength);
      [DllImport("odbc32.dll")]
      private static extern short SQLFreeHandle(short hType, IntPtr handle);
      [DllImport("odbc32.dll",CharSet=CharSet.Ansi)]
      private static extern short SQLBrowseConnect(IntPtr hconn, string inString, short inStringLength, StringBuilder outString, short outStringLength, out short outLengthNeeded);

      private const short SQL_HANDLE_ENV = 1;
      private const short SQL_HANDLE_DBC = 2;
      private const int SQL_ATTR_ODBC_VERSION = 200;
      private const int SQL_OV_ODBC3 = 3;
      private const short SQL_SUCCESS = 0;
      private const short SQL_SUCCESS_WITH_INFO = 1;
      private const short SQL_NEED_DATA = 99;

      private DbEnumerator() { }

      public static string[] GetSqlServers() {
        
string[] ret = null;
        
IntPtr hEnv = IntPtr.Zero;
        
IntPtr hConn = IntPtr.Zero;
        
const string CONNECTION_STRING = "DRIVER=SQL SERVER";
        
const short DEFAULT_RESULT_SIZE = 1024;
        
StringBuilder outString = new StringBuilder(DEFAULT_RESULT_SIZE);
        
int retCode;

         try {
           
// allocate the environment handle
           
retCode = SQLAllocHandle(SQL_HANDLE_ENV, hEnv, out hEnv);
           
if ( SQL_SUCCESS != retCode && SQL_SUCCESS_WITH_INFO != retCode ) return null;

            // set the proper version environment attribute
           
retCode = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (IntPtr)SQL_OV_ODBC3, 0);
           
if ( SQL_SUCCESS != retCode && SQL_SUCCESS_WITH_INFO != retCode ) return null;

            // allocate the connection handle
           
retCode = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, out hConn);
           
if ( SQL_SUCCESS != retCode && SQL_SUCCESS_WITH_INFO != retCode ) return null;

            short lenNeeded = (short)outString.Capacity;
           
retCode = SQLBrowseConnect(hConn, CONNECTION_STRING, (short)CONNECTION_STRING.Length, outString, DEFAULT_RESULT_SIZE, out lenNeeded);
           
if ( SQL_NEED_DATA == retCode && lenNeeded > DEFAULT_RESULT_SIZE ) {
              
// try again with the new length only if more data is needed and the string needs to be longer
              
// otherwise we'll just end up with the same result again
              
retCode = SQLBrowseConnect(hConn, CONNECTION_STRING, (short)CONNECTION_STRING.Length, outString, (short)(lenNeeded + 1), out lenNeeded);
           
}

            if ( SQL_SUCCESS != retCode && SQL_NEED_DATA != retCode && SQL_SUCCESS_WITH_INFO != retCode ) return null;

            // extract the servers out of the returned string and split them on the commas into the return value
           
Regex servers = new Regex(@"^SERVER:Server={(?'SERVERS'.*)};", RegexOptions.Singleline);
           
Match m = servers.Match(outString.ToString());

            if ( m.Success )
              
ret = m.Groups["SERVERS"].Value.Split(new char[] {','});
        
}
        
catch ( Exception ) { /* swallow the exception */ }
         finally {
           
if ( IntPtr.Zero != hConn ) SQLFreeHandle(SQL_HANDLE_DBC, hConn);
           
if ( IntPtr.Zero != hEnv ) SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
        
}

        
return ret;
      }

   } // DbEnumerator class

} // Devstone.Tools.DataAccess namespace

Tuesday, September 14, 2004 9:59:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Friday, September 10, 2004

In preparing my custom server control (webEdit and webMenu) for general distribution, I've taken the time to make the package more of a complete offering.  That is, I would not generally be comfortable simply releasing my code without any explanation of how to use it.  As a direct result of this inexplicable compulsiveness, I've enhanced the package to support design time intellisense.  I am one of  the (few/many?) developers that actually prefers to do all of my design work in the raw .aspx/.ascx environment, rarely if ever relying on the visual designers.  In fact, one of the first things I do on a fresh VS.NET install is turn off the setting to default to design view for HTML files.

In this article, I'd like to venture into the world of creating our own custom Intellisense for your web server controls that is supported in the IDE.  Note: from what I understand, this will not be necessary in the Whidbey timeframe (yipee!), but it will suffice for the time being.

I'll start at the beginning, assuming no knowledge of how to add support for Intellisense.

VS developers have been accustomed (spoiled?) by the prevalence of the code-completion support built into the IDE.  Open any code document and begin typing and you'll quickly see the help appear (e.g. <asp:…).  This assistance appears based on context and can display classes, properties, values, methods, events, and much more.  The Intellisense that appears within the IDE for .cs or .vb files is driven off of reflection.  You don't have to do any work to get it to appear other than change your code.  The Intellisence within the HTML designer, on the otherhand, is driven by metadata.  This metadata file is found in C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml\asp.xsd in a default installation.

If we want to add our own Intellisense, we have two options 1) edit asp.xsd or 2) create our own.  Needless to say, you SHOULD NOT edit asp.xsd.  No, the programming police will not come knock down your door and drag you off (though perhaps they should), but if in a patch or update the file ever gets replaced you'll lose your changes.

Instead we should create our own file and plop it in the same directory.  I start to cringe at this thought, because I don't want to have to maintain two files (one in my \dev folder and one in the VS folder).  I am loathe to have my only file in the VS folder.  If you're developing your software on Windows XP or higher on an NTFS partition (which you should be doing if you're not), here's a cool tip.  If you only want to maintain a single source file on your system (e.g. the one in your \dev folder), and still have it referenced elsewhere without creating a copy (which is easy to forget to do):


fsutil hardlink create "C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml\devstone.xsd" "C:\Dev\......\devstone.xsd"


The FSUTIL tool (File System Utility) allows us to create a hard link so that the file appears in another directory.  ACLs are shared and changes made in one file are reflected in the other because, well, it's the same file.

So to facilitate my development, I created my devstone.xsd file in my \Dev folder and a batch file called 'Register Intellisense.bat' that performs the FSUTIL operation (I only need to run it once, but if I ever get my source to another computer, it'll be there for me to run and I'll be set).

Now to creating our devstone.xsd file:

Begin by creating a new .xsd file and place the following Xml elements therein, changing the namespace to something appropriate for your situation:

<?xml version="1.0" encoding="utf-8"?>
<xsd:schema targetNamespace='urn:http://www.devstone.com/schemas'
   elementFormDefault='qualified'
   xmlns='urn:http://www.devstone.com/schemas'
   xmlns:xsd='http://www.w3.org/2001/XMLSchema'
   xmlns:vs='http://schemas.microsoft.com/Visual-Studio-Intellisense'
   vs:friendlyname='Devstone Web Forms Controls'
   vs:ishtmlschema='false'
   vs:iscasesensitive='false'
   vs:requireattributequotes='false'>

<xsd:annotation>
   <xsd:documentation>
      Devstone ASP.NET Web Forms Controls schema.
   </xsd:documentation>
</xsd:annotation>

</xsd:schema>

In order to create your Intellisense then, you create <xsd:element /> entries that define your valid 'top-level' objects.  Each <xsd:element /> is tagged with a type attribute that identifies an <xsd:complexType /> defined elsewhere in the document:

<xsd:element name="WebEdit" type="WebEditDef" />

<xsd:complexType name="WebEditDef" vs:noambientcontentmodel="true">
   <xsd:attribute name="Runat" type="runat" />
   <xsd:attribute name="Enabled" type="xsd:boolean" />
   <xsd:attribute name="Height" type="ui4" />
   <xsd:attribute name="ID" type="xsd:string" />
   <xsd:attribute name="Text" type="xsd:string" />
   <xsd:attribute name="Width" type="ui4" />
   <xsd:choice>
      <xsd:element name="Toolbar" form="unqualified" type="WebEditToolbarDef" />
   </xsd:choice>
</xsd:complexType>

There are a few things to notice here.  First of all, I've decorated the <xsd:complexType /> with a schema annotation vs:noambientcontentmodel.  That's a special attribute that VS.NET reads and understands that children of this type when entered on the .aspx/.ascx should be limited to the set of valid children as defined in the <xsd:choice />.  By default, you'd get a comprehensive set of the children of the page as well.  In this case we want to see only the <toolbar> element.

Now to get this tied onto our WebForm, we add the @Register directive to the top of the page and then add a reference to the Xml namespace within an element that will (directly or indirectly) be parent to our WebControl.  The <body> tag is a good candidate for .aspx, but since .ascx pages (a.k.a Web UserControls, HTML Controls) don't have a <body> tag, you'll need to find another element, such as a <div>, <td>, or something like that.

<%@ Register tagPrefix="devstone" Namespace="Devstone.Web.WebControls" Assembly="Devstone.Web.WebControls.WebSuite" %>
<body xmlns:devstone="urn:http://www.devstone.com/schemas">

Notice that the Namespace matches identically to the targetNamespace of our schema file.  Also, the devstone namespace matches the tagPrefix within the @Register directive.  This ties our control's Intellisense to that element.  Now we would get autocompletion for our <devstone:webEdit /> control on the page.

There's a lot more to this than what I've posted here in this blog entry, but I've also included my .xsd file for download if you'd like to check it out.

Enjoy!

Friday, September 10, 2004 8:21:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback