Damon Payne: Hand waving software architect

103db signal to noise ratio at < .03% total harmonic distortion
Solution Architect, software developer, geek
Damon Payne at Blogged
2007 Microsoft MVP - Solution Architecture
 Friday, May 01, 2009

The next two stops on the Mix it Up! tour are quickly approaching.  My talk to the Wisconsin .NET Users Group (metro Milwaukee area) has been moved up to this coming Tuesday, May 5th. You can find details on how to register and where to go here.

The following night, Wednesday, May 6th, I will be in Madison.  You can find details on this talk here.

I hope to see you there!



Friday, May 01, 2009 8:00:22 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, March 31, 2009

The What:

A whole lot of goodness was announced at MIX09.  Many of us were unable to attend.  Larry Clarkin and Dave Bost of Microsoft decided to locate some dedicated Web/RIA geeks in the Midwest region to bring an ultra-condensed version of the MIX09 announcements to user groups, conferences, and code camps.  While the level of detail won’t equal what you would have gotten by going to MIX09, we can hopefully get you excited about what’s coming up and answer some of your questions. 

The Who:

Corey Miller

Anthony Handley

Damon Payne

Josh Holmes

Larry Clarkin

The When:

  • 4/1 CD2UG (Corey Miller / Anthony Handley) – The first stop, I’ll be heading down to Chicago tomorrow to provide moral support and take notes on how things flow.
  • 4/9 Indianapolis, IN (Damon Payne)
  • 4/15 CNUG (Corey Miller/ Anthony Handley/ Josh Holmes)
  • 4/30 Lake County (Anthony Handley)
  • 5/6 Madison, WI (Damon Payne)
  • 5/8 RIAPalooza (Anthony Handley)
  • 5/9 Fox Valley Day of .NET (Damon Payne/ Larry Clarkin) – This is a day long conference, and our presentation will serve as the keynote for this event!
  • 5/12 Milwaukee (Damon Payne)
  • 5/12 Ft Wayne, IN (Corey Miller)
  • 5/14 Chippewa Falls, WI (Damon Payne) – hopefully I can be forgiven for looking forward to this date most of all.  Doug has promised to book the Leine Lodge for this presentation.  Beer + Silverlight = shangri-la
  • 5/16 Indy Code Camp (Josh Holmes)
  • 5/19 South Bend, IN (Corey Miller)
  • 5/26 Rockford, Il (Josh Holmes)
  • 5/30 Chicago Code Camp (Josh Holmes – possibly others?)

See you there!



Tuesday, March 31, 2009 9:11:15 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, February 23, 2009

I have taken first place in the most recent Silverlight: Write and Win contest on SilverlightShow.Net for my article on creating a Designer for Image Hotspots in Silverlight.  Thank you judges!



Monday, February 23, 2009 9:53:31 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Friday, November 28, 2008

After some hideous CSS work I have deployed a new default theme for the blog.  I couldn't have done it without the help of CJ from CarSpot; I definately owe him a link on here as soon as I find the URL of his freelance site. 

The old design looked horrid in some browsers, mostly IE 6, 7, and 8 and Firefox 2 and 3...

Some older pages/huge images are absolutely breaking the layout still, but anything newer (posted with Live Writer using <pre> tags for code) should be working and scrolling code.  Please leave me a comment if you notice anything broken and I'll continue to tweak.



Friday, November 28, 2008 12:05:56 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [5]  |  Trackback
 Wednesday, April 16, 2008

I had the most frustrating dasBlog issue over the past couple of days:

I suddenly found myself unable to post anything on my blog.  This coincidentally happened at the exact same time my hosting provider had me try a web.config change to alleviate some session/viewstate issues I was having.  I would try to make a post, get no errors, and be returned to the front page and see that my post was missing.  Referral logs were strangely missing, and the dasBlog event log was also strangely empty.  The only thing I could get a vague error message from was trying to post a comment.

Apparently, my "Anonymous Asp.Net user" had suddenly hit its disk-space quota.  I don't know if this quota was created recently or not.  It seems odd indeed that dasBlog would cruise along and never report an error when it's out of disk space.  I wonder if this is something dasBlog is doing or something that's happening because of the hosting environment?



Wednesday, April 16, 2008 11:39:55 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, April 02, 2008

Upgraded to DasBlog 2, tried my hand at modifying a theme.  It's Orange.



Wednesday, April 02, 2008 7:46:32 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Sunday, March 30, 2008

A small part in my decision to make the leap to Vista 64 was Scott Hanselman's findings upon building his home office and a new workstation therein: moving to 64bit was a non issue.  I wish I could report the same.

My first issue was with installation.  With certain hardware configurations Vista x64 will crash with 4gb of RAM installed until a hotfix is applied.  My understanding of the state of the 64bit world is this: the current 64bit processors are not entirely 64bit.  They have 64 bit registers and some 64 bit instructions.  To the average PC hotrodder or developer the preceived benefit is the ability to use 4gb of RAM instead of 2gb.  I installed the hotfix, got my 4gb of RAM working, and set about playing the Sweeny Todd soundtrack whilst I decided what order to install things in...

The drivers for my X-Fi sound card installed, I started Windows Media Player.  Windows Media Player crashes the first time I start it 100% of the time.  Vista Sp1 and a horde of updates has not fixed this.  Since the X-Fi is probably the most popular aftermarket sound card in the known universe at this time this ought to to work.  It may have nothing to do with sound drivers at all.  The 2nd or 3rd time I launch WMP it will work happily forever.

My graphics card is a PNY GeForce 8800GT series.  It clearly lables itself as having both 32 and 64 bit drivers on the driver disc.  I experienced an new Vista phenomenon with this product: installing the drivers using the OEM program declared that no changes were made to the system.  I pointed Vista at the .inf file on the disc for x64 and Vista basically said its own driver ("Standard VGA Port", yeah right) was better and refused to take the OEM drivers.  Only the Nvidia unified driver install would work.  Odd...

My next stage is to get my various messenger programs up and running so I can chat whilst I wait on things to intall.  MSN messenger is objectively the best messenging experience by far so that went worst, no issues there.  I have used Pidigin for AIM (most CarSpot folks and some friends are on AIM) since having crashtastic experiences with Trillian and an absolutely astounding clusterfuck with the actual AIM product ruining a Vista install; it would seem AOL is the reigning champ for the national heavyweight Invasive Install Championships.  Pidgin has similar behavior as WMP, it will crash and then run perfectly for as long as the machine is up.

My Blu-Ray drive was working fine as a Serial ATA DVD drive.  Folks, installations of things like Office 2007 are actually quite painless with something better than an IDE CD drive.  This alone is worth the extra expense.  Still, it came with software that can play Blu-Ray and there are nights (like tonight) where I'd like to watch a movie on my 1080p monitor while some long-running tasks scroll by on my ancient 19" CRT.  My first attempts at getting Blu-Ray playback working met with failure.  The install process (damn you Pioneer) demanded ridiculously old and specific versions of DirectX that were clearly not going to happen on Vista 64.  Some combination of Windows Update and SP1 magically fixed this, so I watched Blood Diamond on Blu-Ray while logs scrolled by tonight. 

My last two 64bit issues dealt with actual software development.  Visual Studio and the like installed and ran fine except for one issue so infuriating I have a dent in my forehead: copying an ASP.net solution from one machine to another suddenly started claiming that "{MyCustomThinger} RoleProvider has already been added", and commenting this part of Web.config out certainly allows the site to run.  As expected, putting this web.config with the role provider commented out on any other machine (including my development laptop and the two servers where the site actually lies) crashes because of course the configuration is incorrect.  I still can't explain this one.  The next issue was the most time consuming.

MySQL comes in 32 and 64 bit flavors for Windows.  I needed MySQL for one of my development efforts.  MySQL x64 does not like Vista x64.  In fact, the slightly out of date platform notes on their site claims Vista x64 is not supported, but there are enough success stories out there suggesting this is just a CYA that I gave it a whirl anyway.  MySQL 64bit would not run, claimin a "side by side configuration" was incorrect.  OK, knowing very litting about anything this sounds like some sort of thunking layer issue, so I'll try the 32bit version.  Same error.  What does Google say?  Google says that the Application Manifest in the server configuration process is broken for 64bit windows.  The MySQL forums claim that only a program called Reshack can fix this.  The problem with Reshack is that it runs on 32bit platforms only.  For those who don't know: the various Icons, embedded resources, and execution manifests for a Windows .exe get compiled into a special section of a windows PE and programs like Reshack can read/write this area without doing anything to the code itself.  I eventually found a Delphi program posted by a company in the UK that would work for me.  Having had some bad experience with libraries without a strong name that I am missing the code to recompile, I was glad MySQL doesn't attempt to use authenticode signatures with their releases.  I changed the Vista application manifest XML to the appropriate "requireAdministrator" and I was finally off and running.

The last issue I experienced this weekend was with some code I "inherited".  It uses Microsoft Jet ODBC to treat a file as a "data source", access the file rows using DataReader constructs, and sort the contents in a DataGrid.  This code bombed when I ran it.  Google tells me that Jet does not exist for x64.  I had planned on rewriting this code anyway but wasn't up to the task tonight, so I kept looking.  .NET programs are usually targetted to "any CPU" by default, but Google told me changing the target specifically to x86 would allow some extra Thunking Magic to happen, and as soon as I did this I was back on my way with Jet magically found now.

After these experiments, I'm definately going to test all of my production code to make sure I haven't done anything that won't work with 64bit editions of Windows or the .NET framework.  I also need to take it upon myself to do some research: what does 64bit do for me and for Joe Consumer besides 4gb of RAM on Vista?

.NET | ASP.NET | Rant


Sunday, March 30, 2008 1:17:26 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, March 18, 2008

It would appear, at least on my machine, that I can add "No script debugging" to my list of IE8 complaints.  Despite making the necessary Internet Options changes I can't hit breakpoints in javascript in VS2008 any longer.  It's a beta browser and I don't really need this feature, but be warned.



Tuesday, March 18, 2008 9:19:01 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, December 13, 2007

A note to Asp.Net developers:

I cannot think of any valid reason why <div> tags opened in one UserControl should not also be closed in the same UserControl.  In fact, closing <div> tags from UserControl1.ascx in Default.aspx or in UserControl2.ascx makes markup extremely annoying to maintain. 

That is all.



Thursday, December 13, 2007 9:03:55 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, September 25, 2007

ClickOnce is so close to greatness, as I’ve said before.  Yes, I know the “official word” is that this is not really meant for software ISVs deploying software to end users but it seems with a few extra things it could indeed be very good for this.  With some features in the Orcas Beta 2, I think ClickOnce is getting much closer to providing the plumbing I’ve been wanting in some cases and home rolling in others.  A new product I’m working on has some Architectural Desires.  I call them “desires” because we’ve been selling desktop applications without these architectural features for a while but I really want to meet some Service Level Requirements that will make my life easier, the customer’s life easier, and our support staff’s life easier.

Environment

·         I have an existing web system that users sign into.  Credentials are stored in an app-specific schema in a MySQL database.  Users of the current desktop application must be in this MySQL database and authenticate over web services.  Users of the new system increasingly do not need access to the web based system.  Putting their credentials in MySQL is polluting this system.

·         We have an OpenLDAP server we are slowing moving credential stores to.

·         We have a Windows2003 server that’s not doing much right now

·         We have a client application that ships with specialized hardware and an install disk.

·         Upgrades go out to customers via installing a new MSI

Requirements/Desires

1.       My software requires some specialized hardware.  I’d like to ship this without install disks.

2.       I’d like the desktop application to be able to have the Role information and authenticate against our existing credential stores.  I don’t want users to be able to run this within a very short time period if we turn them off.

3.       I’d like to stop polluting the existing web system credential store with users that should not ever be able to log into this system, however SOME users of the web based system will also need to be able to authenticate using the client application.

4.       I’d like to be able to keep random people from stealing software.

5.       I’d like to be able to keep random people from installing my software.

 

Solution in Code and UML

Let’s take the big red pill and see where it leads us.

ClickOnce - Part 1

I start with .NET 3.5 Beta 2, VS 2008 Beta 2, and my current environment.  Setting up ClickOnce deployment is easy enough, I set the application to check for updates before it runs, and verify that this works.  Oddly enough with Beta 2, 100% of my testers who have tried to click “Install” rather than installing .NET 3.5 and choosing “launch” have had the install .exe lock up.  Hopefully this gets fixed.

Client Application Services – Server Side

There is a wonderful bit of plumbing available now Called Client Application Services.  In order to use this you need an ASP.Net web site.  You need to add a reference to System.Web.Extensions, and configure your web.config such that the Role and Membership services are enabled.  With .NET 3.5 this is done like so:

      <system.web.extensions>

            <scripting>

                  <webServices>

                        <authenticationService enabled="true" requireSSL="false"/>                   

                        <roleService enabled="true"/>

Now, the Role and Membership providers I have configured are my LDAP based home rolled code.  Discussing these is outside the scope of this article; suffice to say they are straight up Provider implementations.  I will need to set up my Forms authentication a certain way to meet my single sign on goals:

            <authentication mode="Forms">

                  <forms domain=".office.carspot"

                           enableCrossAppRedirects="true"

                           cookieless="UseCookies"

                           slidingExpiration="false"

                           defaultUrl="Cookies.aspx"

                           path="/"

                           protection="Encryption"

                           timeout="1440"

                           name="FooBar"/>

            </authentication>

For testing I am allowing the clients to sign on once for a day.  The domain attribute is important, as the cookie created by my web site will now be passed to anysite.office.carspot.  There is one more thing to configure here.  At this point I intended that I would manually provide the Symmetric Key to Trusted Applications written in PERL or Python etc. to decrypt the data in the cookie.  I therefore manually created some keys for a MachineKey element.  This will still be needed when I cluster this solution.

      <system.web>

            <machineKey validationKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            decryptionKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            validation="SHA1" decryption="AES" />

Client Application Services – Client Side

With the server side set up, I return to my ClickOnce application.  There will be some work to do in the Properties of my app and some work to do in code.  The first step is to enable Client Application Services.

Only the root directory of the ASP.Net application is required.  This also requires references to System.Web and System.Web.Extensions in my Client application.  This is the first hole I’ve seen thus far in my argument that a Client-only .NET install should be available.  The advanced tab bears some discussion now and also a little later:

In my case I want to make sure they are authenticating to the server at regular intervals.  The Use custom connection string setting is the key to a later problem.  Interested readers may want to use reflector on System.Web.ClientServices.Providers.ClientData,  System.Web.ClientServices.Providers.ClientDataManager, and System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, as well as exploring the new System.IO.IsolatedStorage namespace, unless using Reflector on system assemblies is against some rule, in which case you should not and I never have done so.

Now I need to write some code and see if I can log in.  Note that back in the Services tab I included a Class and Assembly name for a credential provider type.  This tells the plumbing what method to invoke in order to request credentials from the users.  The class must implement IClientFormsAuthenticationCredentialsProvider, in my case this is a login window written in WPF.  Insofar as my research has gone so far, authenticating members and checking roles must be done explicitly.  The code to do so is simple:

bool valid = System.Web.Security.Membership.ValidateUser("", "");

This will cause the plumbing to invoke my Login window and give me an answer as to whether authentication was successful, and since Role management is enabled I should be able to get at my roles as well.  Now I’m going to set up a proxy and try to log in.  Here’s what Fiddler shows me:

 

You can see the AJAX/JSON requests going back and forth, and I’m showing the headers of the response in order to show you the persistant cookie.  The actual body of the response is just {“d”, true}.  Now I’m going to try to get this to authenticate against a Web service written in another language on a different server.

This Cookie is not for you

My thought was to use cookies with a common Domain in order to authenticate against my ASP.NET site via Client Services but have the information available in a usable form to other non-.NET systems.  This turned out to be a pipe dream at first.

·         Authenticating via Login.aspx returns a cookie as expected.  Cookies are turned off for HttpWebRequest by default.  Nothing I could find would get Windows/.NET to nicely populate cookies on my HttpWebRequest once I gave it a CookieContainer.  This is probably desired behavior.

·         Authenticating via Client Services does not create a cookie in the same way IE does anyway (more on this in a moment).

·         Any IE specific hacks I do are likely to be broken in Firefox, and supposedly ClickOnce will work with Firefox later this year.

I started by experimenting with the Use Custom Connection String setting, but realized this may be problematic in ClickOnce since the actual Application directory is not a clean or dependable Path.  When facing a dilemma like this I pull out my handy File System Watcher program and go to work. 

While logging in, this “User_damonpayne.clientdata” file created and changed looks promising, and it turns out to be.  I can already tell by calling Thread.CurrentPrincipal.Identity.Name that my server-side user name “damonpayne”, is indeed available on the Client.  Opening up this .clientdata file reveals the following:

<?xml version="1.0" encoding="utf-8"?>

<ClientData>

      <LastLoggedInUserName></LastLoggedInUserName>

      <LastLoggedInDateUtc>1C6E0B92F1DBE7E</LastLoggedInDateUtc>

      <PasswordHash></PasswordHash>

      <PasswordSalt></PasswordSalt>

      <Roles>

            <item>Default</item>

            <item>Technology</item>

            <item>ScrumMaster</item>

      </Roles>

      <RolesCachedDateUtc>1C7FF8AA84CDD3E</RolesCachedDateUtc>

      <SettingsNames></SettingsNames>

      <SettingsStoredAs></SettingsStoredAs>

      <SettingsValues></SettingsValues>

      <SettingsNeedReset>0</SettingsNeedReset>

      <SettingsCacheIsMoreFresh>0</SettingsCacheIsMoreFresh>

      <CookieNames>

            <item>09a630885b744552b25cf95da4f7e20f</item>

      </CookieNames>

      <CookieValues>

      <item>SoloAuth=9816CAC289A2ADBE7E88A0498E71733E0C480CAD631CB57460BF8AB9822747A43FE1ABE6F0D20DCAA95D8339FE8EC866C878A10A1A2BB188AE42B50DCB4C9862</item>

      </CookieValues>

</ClientData>

Those are indeed my roles, and that does look like my server cookie value.  How convenient.  Of course from the Client I can simply call System.Web.Security.Roles.GetRolesForUser() to get my roles, but remember that I sought to pass a token to someone else that would vouch for who I am.  I will now spare you some of the details of my research and skip to the punch line.  By converting my Hex string from <machinekey/> back to a byte array I was hoping to privately share this Symmetric Algorithm key with trusted systems and allow them to decrypt the cookie value.  Well, this didn’t work, and switching algorithms from AES to 3DES didn’t work, and changing the key sizes didn’t work.  Digging through FormsAuthentication through reflector I found the 1st issue: the data is not plain text but rather a binary serialized object.  Ok, so one could use BitConverter to pull out the desired values.  The real sticking point is that FormsAuthentication, when it creates a Symmetric Algorithm of the type specified by your <machineKey/> setting, creates a random Initialization Vector that is not available to you.  How this voodoo works in a clustered environment I don’t know, but this behavior is somewhat disappointing.   Community Server, for example, allows you to put the IV in the web.config as well since this is a very necessary part of the cryptographic process.  My hopes of sharing a symmetric key with a trusted application were killed, and I spent quite a bit of time on Google and USENET looking for a solution.  This was one of those situations where the obvious answer eluded me for a while because I had been too close to this problem for too many hours. 

I can still write code to dig out the cookie value:

        /// <summary>

        /// Assuming local storage, get the client data cookie for this application

        /// </summary>

        /// <returns></returns>

        public static string GetClientDataCookie()

        {

            string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

            string ver = Assembly.GetCallingAssembly().GetName().Version.ToString();

            appData = Path.Combine(appData, @"CarSpot\CarSpot.Solo\" + ver);

 

            string userName = Thread.CurrentPrincipal.Identity.Name;

 

            string userFile = "User_" + userName + ".clientdata";

            string path = Path.Combine(appData, userFile);

            XmlDocument doc = new XmlDocument();

            doc.Load(path);

            string token = doc["ClientData"]["CookieValues"]["item"].InnerText;

            token = token.Substring(token.IndexOf("=") + 1);

            return token;

        }

… and I still have a public facing Web server that magically (in memory?) has access to the Initialization Vector:

        [WebMethod(Description="Attempt to decrypt a Forms auth ticket issued from this server farm")]

        [SoapDocumentMethod(ParameterStyle=SoapParameterStyle.Bare)]

        public ValidateClientTicketResponse ValidateClientTicket(string ticketString)

        {

            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(ticketString);

            ValidateClientTicketResponse response = new ValidateClientTicketResponse();

            response.Expiration = ticket.Expiration;

            response.Name = ticket.Name;

            response.UserData = ticket.UserData;

            return response;

        }

… and of course, for trusted systems, I can secure this Endpoint however I please.  I can pass my Ticket from the client to anyone, who can call this service to determine if I am who I say I am and if this is a valid, unexpired ticket.

 

Solution Diagram

So, this is my environment with the solution in place.  Users who only need my Client Application and its accompanying Web Application are created in LDAP.  Users of the special Perl web site that also need to have access to my client are pushed to LDAP via a replication scheme.    My client application can pass it’s authentication ticket value as a SOAP header or an in-band argument when communicating with other trusted endpoints. 

Since I worked through this step by step, there may be a slightly cleaner way to do some of this.  I have been meaning to see if WS-Federation could be used, if only as a more standard API for the web service.

Final Thoughts

Of my original requirements, I have met most of them.  I can keep random people from stealing my code by deploying Obfuscated assemblies via ClickOnce.  I can keep people from running my application using the authentication, and with this method many customers will only have a single password to remember for all their CarSpot products.  I did not write about it here but the ASP.Net Profile information is available via this method as well.  This opens up the door for many unexpected niceness for our users; by implementing a new custom Profile provider, someone might log into one of the Perl web sites and change their display preferences, to discover them magically reflected in their client application as well. 

As for keeping random people from ever even installing my software to begin with, that will have to come later or Obfuscation will have to do.  It would be ideal to protect the .application file for my program so that unwanted visitors cannot even download the manifest in the first place.  Are you listening, Microsoft?  ClickOnce should be able to respond to an authorization challenge or SSL certificate warning in some fashion.   In our tests, Firefox still does not work for launching a ClickOnce application. Are you listening Microsoft?



Tuesday, September 25, 2007 11:57:32 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Thursday, September 20, 2007

The installation of .NET 3.5 beta 2 does not function out of the box with Windows Server 2003.  An error regarding unknown attribute xmlns="" is shown starting on line 67 in the machine level web.config.  Removing the 4-5 xmlns="" from the <add assembly/> lines seems to allow my web app to work but I don't know what the other consequences might be.



Thursday, September 20, 2007 1:15:49 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, September 05, 2007

Suppose you are developing an ASP.Net application in a primarily open source/linux/MySQL environment, you might run into some odd issues with interoperability.  A top-secret project of mine (that I intend to open source once I've got a handle on which license is appropriate) needs minimally to work with MySQL and OpenLDAP.  MySQL is no problem as the builds of the MySQL connector for ADO.NET 2 have been very solid, though it does give me odd "warnings" about unnecessary conversions when using reader.GetInt32(0) and such. 

OpenLDAP, well, it seems decent enough for our purposes but there are some stumbling blocks.  The ActiveDirectory Membership and Role providers that come with .NET 2.0 will not work for you.  Oh, I know the connection string misleadingly takes LDAP://ou=blah blah;cn=blahblah;dnblahblah syntax but the implementation uses various AD specific calls so you have to home roll your own.  There are two decent solutions here: you can use the classes in System.DirectoryServices to implement the various searches.  I was concerned at first that this would not work under Mono, which is one thing I'm shooting for, but I should have done my homework as the Mono project does have a System.DirectoryServices implementation.  I also found some old Novell C# code hanging around that gives you some LDAP specific abstractions.  Either way, you can roll your own Role and Membership providers that talk to OpenLDAP.  OpenLDAP does not support the memberOf syntax, apparently, which is annoying.  You can search under the ou=Groups for objects of type posixGroup and get the memberUid attribute to sort of go at it backwards to implement the GetUsersForRole functionality for your Role provider:

public List<string> GetUsersForRoleName(string roleName)

{

List<string> userNames = new List<string>();

string searchPattern = "(&(objectclass=posixGroup)(cn={0}))";

string searchStr = string.Format(searchPattern, roleName);

LdapSearchResults sr = _ldapCon.Search("ou=Groups," + _rootDn, LdapConnection.SCOPE_SUB, searchStr, null, false);

while (sr.hasMore())

{

LdapEntry entry = sr.next();

LdapAttribute att = entry.getAttribute("memberUid");

string[] names = att.StringValueArray;

for (int i = 0; i < names.Length; ++i)

{

userNames.Add(names[i]);

}

}

return userNames;

}

 

I should surely post this project once it's mostly done, as it's potentially quite useful.



Wednesday, September 05, 2007 2:36:18 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, May 18, 2007

I've had some issues with Service Unavailable messages the past several days.  This appears to be related to me creating an Application below the main Application at my site root in order to test something.  This should be supported behavior so I am working on this with my hosting provider.



Friday, May 18, 2007 8:50:51 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, April 19, 2007

We have a cool feature in our backend data hub we call Image Overlay.  Basically given images of a vehicle we can do all sorts of transformations including overlaying text ("Buy this car!"), overlaying images ("GM Certified logo") scaling and matte-ing (shrinking the input image down to have a single color margin to draw text in) and support for various geometric primitives.  Our implementation is one of the few in our industry and is probably far more complete, flexible, and better-er than the few competing implementations.  Our backend uses some PERL stuff for this and I wondered if a .NET engine would be as fast or faster and how it would scale, given that the claim has been made that C# is 1:1 performance with C++ and despite my love of C# I found this claim questionable.  So, supposing I have an input image which is assuredly not a car:

And a tiny screenshot of my most anticipated upcoming PS3 game, LAIR:

And I want to do a few operations in realtime in .NET:

  • create a Matte
  • scale the source image
  • Add some text
  • import the Dragon screenshot from LAIR
  • Time the operation

Here is the source code for entering some parameters, which could obviously come from some other source:

<div>

<h3>Source Image:</h3>

<asp:HyperLink ID="_exampleLnk" runat="server" Target="_blank" NavigateUrl="~/sample.jpg">

View source image

</asp:HyperLink>

<br />

Matte and overlay some text:

<asp:TextBox ID="_overlayTxt" runat="server" Text="go MSFT!"></asp:TextBox>

<br />

Import Img X <asp:TextBox runat="server" ID="_xTxt" Width="2em" Text="100"></asp:TextBox>

Import Img Y <asp:TextBox runat="server" ID="_yTxt" Width="2em" Text="100"></asp:TextBox>

<asp:Button ID="_submitBtn" runat="server" Text="Lay it over" OnClick="_submitBtn_Click" />

<br />

<asp:Label ID="_resultTimeLbl" runat="server"></asp:Label>

<br />

<asp:Image ID="_resultImg" runat="server" />

</div>

... and the code behind....

protected void _submitBtn_Click(object sender, EventArgs e)
{
string getQuery = "Image.aspx?overlay={0}&x={1}&y={2}";
int x;
int y;
int.TryParse(_xTxt.Text, out x);
int.TryParse(_yTxt.Text, out y);
_resultImg.ImageUrl = string.Format(getQuery, _overlayTxt.Text, x, y);
}

... and the result...

So, on a Windows Server 2003 running ASP.Net 2.0, how much code does it take to generate this?  Surprisingly little:

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);

bool high = true;

HighResTimer hpt = new HighResTimer();
hpt.Start();
System.Drawing.Imaging.ImageCodecInfo codecInfo;
System.Drawing.Imaging.EncoderParameters parms;

System.Drawing.Imaging.ImageCodecInfo[] infoz = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders();
codecInfo = infoz[1];
parms = new System.Drawing.Imaging.EncoderParameters(1);
parms.Param[0] = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);

string txt = Request["overlay"];
int x = int.Parse(Request["x"]);
int y = int.Parse(Request["y"]);
Response.ContentType = codecInfo.MimeType;
System.Drawing.Image newImage = new Bitmap(640, 480);
System.Drawing.Image srcImage = System.Drawing.Image.FromFile(Server.MapPath("~/sample.jpg"));
System.Drawing.Image lairImage = System.Drawing.Image.FromFile(Server.MapPath("~/lair.jpg"));
Graphics g = Graphics.FromImage(newImage);

//Interpolation mode, smoothing mode, CompositingQuality,PixellOffsetMode,etc.
if (high)
{
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
}

try
{
//Matte color
g.Clear(Color.LightGray);

//Overlay font
Font textFont = new Font("Verdana", 9.0f, FontStyle.Bold | FontStyle.Underline);
Brush fontBrush = Brushes.Black;
//Scales the image down to matte it
g.DrawImage(srcImage, new RectangleF(0, 0, 600, 440));
g.DrawImage(lairImage, new RectangleF(x, y, 150, 84));
g.DrawString(txt, textFont, fontBrush, new PointF(10, 441));
g.DrawString("Damon Certified used o-scope", textFont, Brushes.Red, new PointF(430, 370));
hpt.Stop();
g.DrawString("RenderTime=" + hpt.Duration.ToString("n4") + "seconds", textFont, fontBrush, new PointF(10, 455));

//Stream
newImage.Save(Response.OutputStream, codecInfo, parms);
}
finally
{
if (null != srcImage)
{
srcImage.Dispose();
}

if (null != lairImage)
{
lairImage.Dispose();
}

if (null != g)
{
g.Dispose();
}

if (null != parms) // Seems to want to save in BMP anyway with this code in
{
parms.Dispose();
}
}
}

And I'd say 6/100th of a second on my very slow server (its an Emachine's POS we confiscated from someone after a pr0n escapade) is pretty good performance.  I have one bug in that it does not always save as a JPEG like I told it to but I'm sure a little research would clear that up.  So there you have it, a simple bit of image watermarking code in C#.

 

 



Thursday, April 19, 2007 11:35:38 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, November 01, 2006

For some reason I had been under the impression that there was not a provider model for session state in ASP2.0.  It seemed a shame that I could not provide my own iplementation for session state in my app.

http://msdn2.microsoft.com/en-us/library/aa479034.aspx shows that this was an incorrect assumption.

You may wonder what could possibly make someone want to implement their own session state provider?  While doing some pie in the sky architectural thinking about my current production environment I noticed that it does not seem that StateServer is clusterable.  I somehow don't think I'll ever get around to digging into this seriously, but I wonder what it would take to create a clusterable StateServer implementation?



Wednesday, November 01, 2006 1:18:27 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Tuesday, September 12, 2006

If you know me, you know I am an audio nut.  A peak at my living room (pictured here) might give you an idea I'm not well in the head, let alone the lunacy involved in the custom home theater that's being built in the basement right now.

The world of audio is a great place to observe tons of Snake Oil being sold and a sad, sad departure from the available Science that could guide us towards better sound.  Its not unlike many areas of computer science, where the Morts have never even heard of the science their day-to-day work is based on.  If you know SQL experts who are great at their job but who weren't aware that the concepts come from relational algebra, set theory, etc. then you know what I mean.

Over the past year I've become very interested in the effect the listening room has on sound.  Audiophiles like me spend a fortune on equipment that has perfect frequency response and near-zero harmonic distortion then put the system in a room that creates 30db peaks and valleys and flutter echos that ruin the stereo image, the list of room problems goes on and on.

I recently created a Room Mode calculator for my audio site, http://www.KlipschCorner.com/.  Room Modes are essentially the frequencies your room will cause to sound louder than they should due to the dimensions of the room.  For example, a 60hz sound wave is 19 feet long making a room with any dimension being 19feet a bad call.  Rooms with the common 8ft ceiling are tough to treat due to the frequency ranges being close to 8ft or 4ft.  A room mode calculator is useful when planning a new listening room because one can quickly see if there will be room modes at any "problem" frequencies and also see the distribution of room modes, where an even distribution is desirable.

There is a graphical component as well as a tabular component.  My hosting provider, while their service is great, chose to license a rather poor charting utility, so if anyone wants to send a free license of Infragistics my way you'd be saving me a lot of pain.

You can see the end results at http://www.KlipschCorner.com/Tools/ModeCalc.aspx and I suppose I can make the KlipschCorner.Acoustics library as it stands now available via some open source license.

I wonder if DirectSound could be made to do some time-domain analysis (using test tones and a microphone for input) for calculating sound decay rates vs ambient noise in a room?

.NET | ASP.NET | DirectX | Rant


Tuesday, September 12, 2006 9:41:47 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [6]  |  Trackback
 Tuesday, July 25, 2006

I am doing a new gig at the Land of Beer, Miller brewing.  Sean McCormack got me involved in an agile project here on a short time frame so I've not had a ton of time to blog lately.  There's a lot to talk about as this project slows down though.

I cannot say that I am a test driven development expert by any means.  However, upon arriving on this project and trying to retro-implement unit tests for a bunch of code that had no tests before I have come to a new understanding of the value of unit tests for regression testing, and for a specific aspect of the TDD paradigm.  Upon arriving and going over the projet I'd seen that the team lead was printing out class diagrams, going through the unit test packages, and checking off Method names that had a test.  I had kept meaning to look at code coverage tools in the past and the sheer amount of tests odewe needed made me look a little more seriously.  As it turns out they were already using TestDriven as part of the developer setup.  TestDriven is by itself a great tool and will have a place in my toolbox henceforth.  TestDriven installs an interesting right-click menu in VS2005 that gives you various options for running tests at the class/method/project/solution level.   More interesting to me at the time was the "Test With --> Coverage" option.  If you don't have Team System it tells you to go install NCover which I had not used before.  NCover is actually a very solid code coverage tool.  You can probably see where this is going and certainly TDD experts will file this under the "duh" department.  TestDriven + NCover + NUnit = tells you how much of your code is covered by unit tests.  NCover is smart enough to show you if, for example, you covered all the various "if()" branches inside a method and such.  This is Very handy and not just for unit tests.

We found, in general, that the application had "thong-level coverage" from unit tests; as any good christian will tell you the thong leaves too much uncovered!  We strove to get it to "Burqa-level" coverage and made a lot of progress before the timeline demanded we start on new development.

Upcoming rants:

"About Casting"

"Funny Stuff I've heard at work recently"

"Indirect paths to success"

Looks like I have some invalid HTML in here somewhere, the visual styles on the links are hozed.



Tuesday, July 25, 2006 9:32:51 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, June 29, 2006

I just completed an "alpha"  version of a for-fun website related to my audio hobby: http://www.klipschcorner.com/

I have not had to do much in the way of web design for a while but I set out to make this design work using only CSS and DIV tags, tables are the way of the past or so I read.  I had a fairly frustrating time at this.  The last time I had to do any kind of real layout was a couple of years ago.  It seemed that you could pretty easily support a good layout with Firefox and IE without creating conditional code by using some basic CSS stuff and tables for layout.  With the current CSS implementations of IE, Firefox, Safari it seems that I'm back in 1998 with even some very basic things looking radically different in each browser.

 



Thursday, June 29, 2006 12:19:28 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, March 27, 2006

This was somewhat annoying.

Regular expressions are one huge area where I know absolutely nothing.  I used to get by in my unix days with stuff like "find . -name *fork* -ls {} " but that's about it.  Tonight I was working on a toy website and got my PasswordRegularExpression right in my <asp:CreateUserWizard/>, however it would still complain about the password.  As it turns out the AspMembershipProvider can specify its own password requirements which trump what you put in your Controls.  Clear as mud.  You can see this in machine.config and override it in your own app if you please:

<membership>

<providers>

<clear/>

<add name="AspNetSqlMembershipProvider"

type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

connectionStringName="LocalSqlServer"

enablePasswordRetrieval="false"

enablePasswordReset="true"

requiresQuestionAndAnswer="true"

applicationName="/"

requiresUniqueEmail="false"

passwordFormat="Hashed"

maxInvalidPasswordAttempts="5"

minRequiredPasswordLength="5"

minRequiredNonalphanumericCharacters="0"

passwordAttemptWindow="10"

passwordStrengthRegularExpression="" />

</providers>

</membership>

Seems somewhat annoying for "configurable things that are there to save me time."

Also, I must get a regex book immediately as my lack of knowledge on this topic wasted all of my playtime tonight.



Monday, March 27, 2006 9:22:16 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, February 13, 2006

I have a small web site I was looking into converting to ASP.NET 2.0 for various reasons *cough resume cough couh* however due to environmental restrictions (corporate politics, not EPA) I have to use MySQL as the data store, and there is an existing schema which is absolutely nothing like the schema the default ASP providers use.  This provided the perfect excuse to dig into some internals and see about providing some Providers. 

Why would one go through the trouble of implementing these interfaces?  First, as you'll see, its pretty easy.  Second, by doing this you can still use all the great plumbing for role-based security and the "Login" and "User" family of controls that ship with ASP 2.  By looking through my machine.config I can see that I'll need to create and configure instances of two abstract classes: System.Web.Security.MembershipProvider and System.Web.Security.RoleProvider.   There are some intermediate classes you could choose to inherit from like SqlMembershipProvider that might save you some time but in my case it seemed quick enough to start at the base class.  Intellisense and the compiler will tell you what methods you need to override.  For example, MembershipProvider requires an implementation for ValidateUser such as:

public override bool ValidateUser(string username, string password)
{
bool valid = false;
EmployeeDataAccess dao = new EmployeeDataAccess();
Employee e = dao.FindByLoginAndPassword(username, password);
...

Simple enough.  If you decide to do this, you may wish to set some breakpoints in methods like RoleProvider.GetRolesForUser and run through your app. 

public override string[] GetRolesForUser(string username)
{
SecurityDataAccess dao = new SecurityDataAccess();
List<string> roles = dao.GetRolesForUser(username);
return roles.ToArray();
}

In my case there is a small performance hit for running this method.  ASP calls this method to validate your access to pages, proces any LoginView controls trim asp:Menu items if you have securityTrimming enabled, and user code could always call Page.User.IsUserInRole("");   sliding expiration in the ASP.NET cache is a good idea here.  Finally, you just need to tell your web application to use your custom providers.  Supposing my security data store is called "IDMS" and my classes are named accordingly, here are my web.config settings:

<membership defaultProvider="IDMSMembershipProvider">

<providers>

<add name="IDMSMembershipProvider"

type="IDMSMembershipProvider"

requiresQuestionAndAnswer="false"/>

</providers>

</membership>

<roleManager enabled="true" defaultProvider="IDMSRoleProvider">

<providers >

<clear/>

<add name="IDMSRoleProvider" type="IDMSRoleProvider" />

</providers>

</roleManager>

So, as you can see, its easy to implement classes to plug into Asp.NET security.



Monday, February 13, 2006 9:15:14 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, November 25, 2005

ObjectDataSource, I hardly knew ye. 

Once I did get to know you, I didn't like you.  You see, when I saw the ObjectDataSource demos I thought "Finally, Microsoft understands me".  I did a couple of quick tests of the designer support and functionality and was pleased with the results.  Tonight I created a "service agent", a class to wrap access to an out of process call, in this case playing around with the Amazon.com affiliate program again.  For operations that require input parameters to the ODS you can often just tie the parameters to a control on your page using markup and with one line of code:

protected void searchBtn_Click(object sender, EventArgs e)
{
  amazonDs.DataBind();
}

gets the results out of the service agent class.  The ODS pulls the parameters our of the configued form fields and away we go.

Then I realized I am still a misunderstood objects maniac lost in a world of DataSet coding.  Once my List<CustomType> is read from my middle tier object it no longer exists.  Anywhere.  I had deluded myself into thinking that things like "SelectedRow" in a GridView would give me something I could cast to my CustomType, that various GridViewEventArg/ObjectDataSourceEventArg.DataItem would give me something I could cast back to my custom type.  Maybe GridView.SelectedRow would contain a set of values I could use to rebuild my entire object...

No, no, no.  I will say for some types of things the ODS is still useful.  As long as you don't mind looking up your objects using a DataKey and/or your model fits in with this a little bit better than mine does I would still prefer this over an SqlDataSource  So unless I find some obscure method that is not presenting itself right now I am back to programming the same way I did in ASP 1.1: Get ArrayList of Objects, put in session to refer to them later, configure <Columns> in Grid control, DataSource, DataBind, etc.



Friday, November 25, 2005 9:42:28 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, November 04, 2005

I had a small master page issue which I fixed quickly but which made me want to complain.

One of the great things about .NET is the great design time experience.  Dropping designer support for nested master pages irked me, and there are some other quirks as well.  In one case, I have a page using <div> and the "float" style attributes for layout.  As soon as the divs are nested three deep or so the content no longer renders in the designer.  It seems to be specifically related to the style as I will explain along with the next issue.

In my master page for the site I import a style sheet like so:

<style type="text/css">@import url(kcstyle.css);</style>

Obviously this is a relative path.  If you create sub-directories beneath your site obviously this won't work so I changed to an absolute path like so:

<style type="text/css">@import url(/KlipschCorner/kcstyle.css);</style>

When the content renders in the designer now it does not show my styles for any pages regardless of directory.  When I run the site however all pages look great regardless of directory.  Now that the designer can't find the stylesheet, the content which did not render inside my <div> tags before is now visible in the designer.  I would assume the designer is in some way using an IE component to render behind the scenes but this might not be the case.  The answer seems likely to be the fact that the "ASP.net Development server" is not behaving in the same way that running the site via IIS would.  The inconvienient part is that if you are using newer standards like <div style="float:left;"> for your layout, the designer view looks nothing like the fully rendered view.

Compare this view without style:

To this one when the site is actually ran:

The only difference is the stylesheet.  Maybe I'm complaining about something petty here, but it seems that this type of thing is going to annoy a lot of people.  Isn't part of the point of the designer that you don't have to run it to see what it looks like?



Friday, November 04, 2005 2:25:00 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Monday, October 31, 2005

So, working on my first web app using ASP 2.  I had really only messed around with the mobile stuff during the betas.  My goal here is to see how robust the built-in controls and frameworks are.  On the surface everything seems very nice and configurable but we'll see what things are "toys" and what things are worthy of serious consideration.

Visual Studio installs SQL 2005 express by default.  This caused me some woe today since I uninstalled Express and installed Developer Edition instead.  Using the site admin tool, my AspNetSqlProvider is not working, and suggests I run aspnet_regsql.  Did that, did not fix it although it did create the membership tables in my SQL 2005 database.  Hmm, looking at machine.config and did a search for AspNetSqlProvider.  A connection string called "LocalSqlServer" is referenced everywhere and the value was...

<connectionStrings>

<add name="LocalSqlServer" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" providerName="System.Data.SqlClient" />

</connectionStrings>

Well then, that won't work if I just got rid of Express.  I suppose there really was no need to un-install express after all if its just an Access replacement and isn't running a service taking up my CPU cycles?  I'm just guessing by looking at the connection string here.

At any rate, in your web.config you can do the following

<connectionStrings>

<remove name="LocalSqlServer"/>

<add name="LocalSqlServer" connectionString="data source=BigFatLaptop\SQL2005;Your info here" providerName="System.Data.SqlClient"/>

</connectionStrings>

and then it should work for you.  More observations as I get time to mess around.



Monday, October 31, 2005 5:20:13 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Sunday, June 12, 2005

Nothing super interesting to report from Friday: I saw a few of the obligatory ASP 2 demonstrations.  I don't know why I'm not tired of these yet.  Perhaps it is because I've done more web development than anything else in my career and its nice to see the developer story finally getting close to where I think it should be.

That wraps up TechEd for me.  All in all, I give it a B.  It accomplished my goal of getting me excited about work again, but a lot of the content was not as in-depth as I would have liked.  I think I'd like to try doing one conference a year.  Next year I think I might try the PDC.

Now that I'm back I have a to-do list a mile long, including catching up on all my side work and trying to make up as much of the 40 hours I missed last week as I can.



Sunday, June 12, 2005 12:33:07 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Wednesday, May 25, 2005

Today's By Request items, taken from the google search records.

log4net from ASP.Net

There have been some searches talking about how to configure log4net to work within your ASP.NET application.  I admit its not as straightforward as it should be.  There are 3 things to remember:

  1. Whatever user your ASP.NET application runs as needs permissions to whatever resources you are trying to access.  For log files this includes create/modify perms in the directory you want to log to.
  2. You need to add a log4net config section handler and config section to your web.config file:

<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
    <log4net>
        <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
            <param name="File" value="C:\\Projects\\TheShaft\\MobileServerEndpoint\\ShaftLog.txt" />
            <param name="AppendToFile" value="true" />
            <param name="RollingStyle" value="Date" />
            <param name="MaxSizeRollBackups" value="10" />
            <param name="StaticLogFileName" value="true" />
            <layout type="log4net.Layout.PatternLayout">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n" />
            </layout>
        </appender>
        <root>
                <level value="DEBUG" />
                <appender-ref ref="RollingFileAppender" />
        </root>
    </log4net>

  3. In the Application_Start event of your Global, you need to tell log4net to configure itself:

        protected void Application_Start(Object sender, EventArgs e)
        {
            log4net.Config.DOMConfigurator.Configure();
            ILog log = LogManager.GetLogger( GetType() );
            log.Info("Application_Start");
        }

And...

Compact Framework 2 on CE.Net

Right now, in Beta 2, you can run CF2 applications on WindowsMobile 2003, CE.NEt 4.0 and up, and WindowsMobile Smartphone.  If you start a SmartDevice CAB project, you will see the "MinVersion" parameter equal to 4.0.  However, I have been told directly from a Microsoft employee (via the newsgroups) that this will not be the case when Visual Studio 2005 goes final.  For the CE.NET OS, only version 5 will run Compact Framework 2 applications.

 



Wednesday, May 25, 2005 8:14:03 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, January 26, 2005
Master Pages in .NET 1.1


Wednesday, January 26, 2005 12:00:00 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Tuesday, December 21, 2004
Tired of Javascript


Tuesday, December 21, 2004 12:00:00 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback