Tuesday, April 29, 2008
I am thrilled to announce that INETA user groups now have an open invitation to use SiteFinity and the offer is complete with free hosting from Discount ASP. Check it out at http://www.sitefinity.com/product/features/free-offer.aspx  Also, take a look at the NETDUG site. We have taken full advantage of this offer and we are very happy with SiteFinity and Discount ASP.
4/29/2008 10:18:44 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, March 14, 2008
The ASP.NET 2.0 DefaultButton property on Form and Panel allows IE to fire a button or link when the user hits the enter key while in a form field.  The LinkButton control does not fire when the users browser is FireFox.  Several people have found creative ways to solve this problem.  I chose to take advantage of .NET 3.5 support for Extension Methods and just wrap the needed fixes into a method right on my form object.  Here is the code for you to drop into your own library.  Perhaps I will do the same for Panel when the need arises.
  
 public static class BrowserHelper
    {
         public static void DefaultLinkButton(this HtmlForm form, LinkButton defaultButton)
        {
            // Inspired by: http://kpumuk.info/asp-net/using-panel-defaultbutton-property-with-linkbutton-control-in-asp-net/

            // Wire-up the Link Button default supported in ASP.NET
            form.DefaultButton = defaultButton.UniqueID;

            // Script to wireup a click event for FireFox
            string _addClickScript = "addClickFunction('{0}');";

            string _addClickFunctionScript =
                @"  function addClickFunction(id) {{
            var b = document.getElementById(id);
            if (b && typeof(b.click) == 'undefined') b.click = function() {{
                var result = true; if (b.onclick) result = b.onclick();
                if (typeof(result) == 'undefined' || result) {{ eval(b.href); }}
            }}}};";

            form.Page.ClientScript.RegisterStartupScript(typeof(LinkButton), "addClickFunctionScript", _addClickFunctionScript, true);

            // Execute the script when the Page loads
            string script = String.Format(_addClickScript, defaultButton.ClientID);
            form.Page.ClientScript.RegisterStartupScript(typeof(LinkButton), "click_" + defaultButton.ClientID, script, true);
        }
    }
3/14/2008 11:51:41 AM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, December 22, 2007
My son has a school video project and when I tried to load my copy of Pinacle 10 on Vista it warned me of incompatibility issues. I decided to pursue something Vista compatible and Adobe Elements looked like a nice option. Unfortionately it crashes so much that we can't even use it. I am going to try it on XP, but if that doesn't do it for us then we may be forced to live with Pinnacle 10 on Vista for a bit. I dont feel like dumping more money into products at this point. Surely it has to work better than Premiere 4! I expected better from Adobe. How could this product pass QA?
12/22/2007 8:40:47 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Tuesday, December 04, 2007
Some Microsoft employees have really cool jobs. Some fo the Developer evangelist types are planning a Code on the Road trip for 2008. Can you say geek out!

Keeps tabs at thecodetrip.com

12/4/2007 4:40:08 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, November 20, 2007
Registration link: http://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032360765&Culture=en-US

Thursday, December 06, 2007 1:00 PM - Thursday, December 06, 2007 5:00 PM

Overland Park Cinemas
7051 Overland Road
Boise, Idaho 83709
United States

Audience: Developer.

Event Overview ATTENTION DEVELOPERS! Are you ready to be one of the first to get their hands on Visual Studio 2008? Join Microsoft, the Boise .NET Developers User Group (NETDUG), and the Boise Software Developers Group (BSDG) for a Visual Studio 2008 InstallFest and Holiday Party on December 6th, 2007 at the Overland Park Cinemas in Boise. Visual Studio 2008 is HERE and this event is your opportunity to get your hands on the released version before anyone else. Every person that installs Visual Studio 2008 on their computer at the event will receive a FREE fully licensed copy of Visual Studio 2008 Professional in the mail shortly after public release. Don't miss out a great evening of food, fun, and your very own copy of Visual Studio 2008 Professional! Space is limited so register today.
11/20/2007 9:57:26 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Monday, July 02, 2007
When hosting Google Video's on your site it can be handy to pull back the thumbnail for the video image. Currently there is no API for doing this so I wrote a method to do it for me. First I retrieve the RSS feed for the video. Then I pull out the thumbnail path from that feed.

        private static string GetGoogleThumnailPath(string contentID)
        {
            string path = null;
            // Fetch the RSS for the video
            WebRequest req = HttpWebRequest.Create("http://video.google.com/videofeed?docid=" + contentID);
            req.Method = "GET";
            WebResponse response = req.GetResponse();
            XmlTextReader stream = new XmlTextReader(response.GetResponseStream());
            XPathDocument doc = new XPathDocument(stream);
            XPathNavigator nav = doc.CreateNavigator();
            XmlNamespaceManager manager = new XmlNamespaceManager(nav.NameTable);
            manager.AddNamespace("media", "http://search.yahoo.com/mrss/");
            XPathNodeIterator iterator = nav.Select("/rss/channel/item/media:group/media:thumbnail/@url", manager);
            while (iterator.MoveNext())
            {
                path = iterator.Current.Value;
            }

            return path;
        }

We used this code on the Pets Best Community site. By the way if you are interested in Pet Insurance you can use my Promo Code: CISAKSON5 for a 5% discount. If you do not see the box to enter it on the Get a Quote page then it probably has automatically been applied through one of my links.
7/2/2007 5:25:46 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Tuesday, October 03, 2006

I am leaving the country and will not be able to use my 2 airline vouchers.  If you can use them throw a bid at them over on EBay!

http://cgi.ebay.com/ws/eBayISAPI.dll?ViewItem&item=140038192577

10/3/2006 9:42:21 AM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, September 30, 2006

For those who are friends or family or just plain curious we are moving to Germany in a couple of weeks.  I have setup a new website and blog for sharing stories and photos about our life and experiences.  It is a family site and several of us will be making use of it.  If you are curious you can check out the new blog at: http://www.isaksonfamily.net/Blogs/tabid/53/BlogID/5/Default.aspx

I now have 3 blogs.  Option Strict for my techie life, the new family blog, and my Jesus blog for sharing thoughts on the Church and spiritual life.  I decided to aggregate all of my blogs on a single page with a single feed over on the new family site for anyone who cares to follow all 3 in one place.

Come see us in Germany.  We look forward to making new friends and seeing old ones while we are there.

Family | Life
9/30/2006 12:20:58 AM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Friday, September 29, 2006

Some of you are requesting example applications that are using the PageFlow tools.  Please be patient.  I am working out bugs in the project on a real world rewrite of the Pets Best enrollment process.  Until I have it working well enough a robust example application is not possible.  There are still some aspects of workflow that are not completely clear and thus result in some challenges in the toolkit.  It is mostly working.  The page flow aspects are working great to drive navigation.  I am now pushing through the remaining challenges with sending objects (data) from a workflow back to the application in a way that will work consistently with persisted workflows.

9/29/2006 5:35:53 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, September 26, 2006

In a few weeks I will be starting a new job in Germany.  As a result I have a couple large items I need to sell.  Contact me if you are interested.

1997 Suburban 1500 : 82,500 miles - $7750 (Boise)


See photos at: http://www.flickr.com/photos/14596724@N00/sets/72157594301357769/detail/

Excellent Condition
Seats 9
4WD shift on the fly
Electric everything
Rear air/heat!
CD / Cassette

Beautiful 2066 sq. ft. house : $229,000


MLS ID: 98263218

4+ bedrooms
3 full bathrooms
excellent neighborhood
walk to schools / parks
cheap utilities

3668 N. Tee Ave, Boise, ID  83704

9/26/2006 7:52:42 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, September 19, 2006
After a couple months of being sidetracked on a different project I am now back on the ASP.NET Page Flow project again.  I am pleased to announce that I also have a CodePlex project for it.  I encourage everyone who is interested to join the project, get the code and start making suggestions and contributions.  I will be putting it to its first use on the Pets Best Enrollment process.  Over the next 4 weeks I will be working with the Pets Best team to perfect the starter kit to a point where it can be trusted for a production release with their Pet Insurance enrollment process.
9/19/2006 5:21:42 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, September 05, 2006

Today I received an update to a suggestion I made to Microsoft on April 8, 2005.  http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=102180  That was before Beta 2 of Whidbey if memory serves me right.  My suggestion was to add features to the Membership API that would allow for a user to be renamed.  There are many scenerios where we choose to use the full Email address of a user as their username instead of having a seperate username.  This works great until the user changes their Email address.  (Another reason to have a permanent address that is not affiliated with your ISP)  I need to be able to allow users to update their Email address and thus their username/userid. 

Apparently my suggestion has been considered for Orcas, but isn't going to make it.  Still, I thought it was good that it was at least considered.

In the meantime, I have found that I can rename a user by directly updating the membership database.  Microsoft cautions against this when using multiple stores for Membership, Roles, and Profile data because user + application is the identity that is shared across disparate stores.  It is my opinion that they should have used a unique identifier instead.  The MembershipUser supports such an identity already.  Similiarly I wish Windows authenticated users and forms authenticated users shared a common data type for their identities.  I believe Microsoft would say that indeed they do and this is the string type that the username or User.Identity.Name.  For my purposes I created a database table to map a GUID to Windows identities so that I could normalize all of my UserId's to be unique and meaningless.  A Email address as a username carries meaning.  That makes a bad primary data key in many scenarios because of the lack of good support for renaming.  Keys should never need renaming.  Even a Windows UserId can change when its based on a persons name and their legal name changes.

What do you all think?

 

9/5/2006 9:35:09 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Friday, September 01, 2006

Yesterday I was faced with a scenario that I have encountered many times before.  I am developing a system in which I will be tracking things like articles and putting them into categories.  I like my code to follow common patterns of abstraction and Domain design models.  Users of the dataset get change tracking for free.  Its time for the Domain Object community to get that as well.

The scenario:  Domain object Article has a property called Categories that is a collection (think List<Category>).  The persistence medium is a database that has 3 tables for articles, categories, and an article to category linking table.  The UI must show all possible categories, indicate the ones the article is already in and handle changes by the user.

The shortcut:  In the past my solution to this scenario was to simply remove the article from all categories and then add it back to the ones that were selected.  That solution is very simple because you never have to worry about which categories the article was in before the update.

The ChangeAwareList<T>:  As I pondered my previous shortcut solution to this problem I concluded that it was time to do it right.  After all, some systems will have a problem if you are always recreating the article to category mappings.  At a minimum the database probably has to re-index the data that may have already existed, but was just deleted and recreated.  Also, the linking table may have other data in it that is being used elsewhere and the shortcut solution could cause problems in those scenarios.  What is needed is a Domain class that can track the changes to a collection.  Note, this is not for tracking changes within an object.  The would be handled with a base class or an interface that tracks the state of an object.  ChangeAwareList<T> is about tracking the state of a collection.

ChangeAwareList<T> inherits from List<T> and adds support for change tracking.  It can be used anywhere a List<T> would be used.  In my case I created a Categories class that simply inherits from ChangeAwareList<T>.     public class Categories : ChangeAwareList<Category> {}

In my data access layer my Get operations load up the collection, turn on the change tracking, then hand it to the business layer.  When the business layer hands back the collection it simply deletes all the items in the RemovedItems collection and adds all the items in the AddedItems collection.  Anything that did not change is left alone.     ChangeAwareList<T> allows you to work with the original collection, the new items, the deleted items, or the updated collection.

Here is the code for anyone interested.

  public class ChangeAwareList<T> : List<T>
   {
      List<T> _addedItems;
      List<T> _removedItems;
      bool _trackChanges;

      public ChangeAwareList()
      {
         _trackChanges = false;
         _addedItems = new List<T>();
         _removedItems = new List<T>();
      }

      #region Clear
      public new void Clear()
      {
         if (_trackChanges)
         {
            _removedItems.AddRange(base.FindAll(AlwaysTrue));
         }
         base.Clear();
      }
      #endregion
     
      #region Remove
      public new bool Remove(T t)
      {
         if (_trackChanges)
         {
            _removedItems.Add(t);
         }
         return base.Remove(t);
      }
     
      public new void RemoveAt(int index)
      {
         if (_trackChanges)
         {
           _removedItems.Add(base[index]);
         }
         base.RemoveAt(index);
      }

      public new void RemoveRange(int index, int count)
      {
         if (_trackChanges)
         {
            _removedItems.AddRange(base.GetRange(index,count));
         }
         base.RemoveRange(index, count);
      }
     
      public new void RemoveAll(Predicate<T> match)
      {
         if (_trackChanges)
         {
            _removedItems.AddRange(base.FindAll(match));
         }
         base.RemoveAll(match);
      }
      #endregion

      #region Add Methods
      public new void Add(T t)
      {
         if (_trackChanges)
         {
            _addedItems.Add(t);
         }
         base.Add(t);
      }
     
      public new void AddRange(IEnumerable<T> collection)
      {
         if (_trackChanges)
         {
            _addedItems.AddRange(collection);
         }
         base.AddRange(collection);
      }
      #endregion

      #region Insert
      public new void Insert(int index, T t)
      {
         if (_trackChanges)
         {
            _addedItems.Add(t);
         }
         base.Insert(index, t);
      }

      public new void InsertRange(int index, IEnumerable<T> collection)
      {
         if (_trackChanges)
         {
            _addedItems.AddRange(collection);
         }
         base.InsertRange(index, collection);
      }
      #endregion
 
      #region Change Tracking
      public List<T> AddedItems
      {
         get
         {
            return _addedItems;
         }
         set
         {
            _addedItems = value;
         }
      }

      public List<T> RemovedItems
      {
         get
         {
            return _removedItems;
         }
         set
         {
            _removedItems = value;
         }
      }

      public List<T> OriginalItems
      {
         get
         {
            List<T> original = base.FindAll(AlwaysTrue);

            foreach (T t in _addedItems)
            {
               original.Remove(t);
            }
            foreach (T t in _removedItems)
            {
               original.Add(t);
            }
            return original;
         }
      }

      private bool AlwaysTrue(T t)
      {
         return true;
      }

      public bool EnableChangeTracking
      {
         get
         {
            return _trackChanges;
         }
         set
         {
            _trackChanges = value;
         }
      }
      #endregion


   }

 

9/1/2006 8:50:31 AM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, July 18, 2006

I just finished blowing an afternoon fighting an issue in ASP.NET 2.0 and crawler detection.  There is a slick feature where you can define browser capabilities with .browser files.  My tests were indicating that Yahoo (Slurp) and Ask (Teoma) were not being detected as crawlers by the framework.  Simple enough.  I'll just create a .browser file and add it to App_Browsers in my app.  In the file I will do a User Agent match on Slurp and Ask. 

A few hours pass by.....its not working....dive into reflector....find some hard coded string having to do with crawler detection.

Take a look at System.Web.Configuration.BrowserCapabilitiesFactory.CrawlerProcess(NameValueCollection headers, HttpBrowserCapabilities browserCaps).  In there is a curious little string "crawler|Crawler|Googlebot|msnbot" being passes to a RegEx processor.

It looks like Yahoo and Ask crawlers are detected as Mozilla browsers, but not as crawlers.  Google and MSN on the other hand are detected as both.  Curiously Google and MSN are both in the little hard coded string that is compiled into the assembly.

My theory is that the browser capabilities detection sees these popular crawlers as Mozilla browsers first and then fails to detect that they are crawlers.  Only those with a user agent string matching what is coded in the framework also get flagged as crawlers.  If anyone has more insight, please comment.

So, to fix the issue I ended up implementing my own wrapper.  Here you go:

      public static bool IsCrawler(HttpApplication app)
      {
         bool isCrawler = app.Context.Request.Browser.Crawler;
         // Microsoft doesnt properly detect several crawlers
         if (!isCrawler)
         {
            Regex regEx = new Regex("Slurp|slurp|ask|Ask|Teoma|temoa");
            isCrawler = regEx.Match(app.Request.UserAgent).Success;
         }
         return isCrawler;
      }

7/18/2006 5:54:01 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [5]  |  Trackback
 Monday, June 26, 2006

This post will introduce the first part of a starter kit for doing Page Flows in ASP.NET with Windows Workflow.  The first Release Candidate for Workflow is now out as part of the June .NET 3.0 CTP.  If you are already working with the Beta's you will need to update your SQL Persistence and Tracking databases with the new scripts.

I have put in a request to CodePlex to host the starter kit.  I will add an update to this blog once it is approved.

The first thing the starter kit contains is the necessary configuration to get a PageFlow project started.  As I started learning workflow I discovered that there are several things required for using it from an application.

Hosting the Runtime

Windows Workflow does not offer any out of the box servers or special applications to take advantage of it.  It is entirely up to developers to host the Workflow runtime inside of an application.  This could be a Windows service, a .NET web service, a Windows application or an ASP.NET application.  The only requirement is that host can work with the .NET 3.0 Workflow assemblies.  In our case we will focus on hosting inside of ASP.NET.

The runtime only needs to be created once per application in order to host multiple workflows.  The blogs and examples I have found so far recommend creating the runtime in the global.asax.cs file.  I have chosen in the starter kit to instead use an httpModule.  By using a module it is easier to add Workflows to existing applications without having to cut and paste code.

All that is needed is to create an instance of WorkflowRuntime and then shove it into an application variable that we can access later in our ASP.NET pages.  We will use this variable in a PageFlowManager control so that direct interaction with the runtime is not even necessary from our applications. 

               WorkflowRuntime workflowRuntime = new WorkflowRuntime("WorkflowRuntime");
               context.Application["WorkflowRuntime"] = workflowRuntime;
               workflowRuntime.StartRuntime();

There are 2 important additional items to note in the code above.  When creating the runtime the constructor takes a configSectionName as string.  That string matches the workflow configuration element name in the web.config.  I find that consistently naming it keeps it simple and easy to manage.  The code would have to be changed if you named it something different.  The second thing is that StartRuntime is called.  This allows the runtime to read the configuration and initialize all of the services that you have added via the configuration itself.

Configuration

Lets take a look as what is in the basic configuration.

First we define a config section for the WorkflowRuntime.  Note that the name matches the name passed to the constructor when creating the runtime.

<configuration>
   <configSections>
      <section name="WorkflowRuntime" type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
   </configSections>

We add a reference to the httpModule that initializes the runtime for us.

   <system.web>
      <httpModules>
         <add name="AspNetPageFlow" type="AspNetPageFlow.WorkflowRuntimeModule, AspNetPageFlow"/>
      </httpModules>
  <system.web>

Finally we configure the runtime with the typical services we need for most workflows.  Note that the root element of the section matches the name given when creating the configuration section above.  The services added via configuration are automatically loaded by the runtime when it starts.  Services can also be added via code, but for the common ones it makes sense to me to just put them in config.

   <WorkflowRuntime Name="WorkflowServiceContainer">
      <CommonParameters>
         <add name="ConnectionString" value="Data Source=.\SQLEXPRESS;database=WFServices;Integrated Security=True;"/>
      </CommonParameters>
      <Services>
         <add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
         <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" UnloadOnIdle="false"/>
         <add type="System.Workflow.Runtime.Tracking.SqlTrackingService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" UseDefaultProfile="true" IsTransactional="false" ProfileChangeCheckInterval="60000" PartitionOnCompletion="false"/>
         <add type="System.Workflow.Activities.ExternalDataExchangeService, System.Workflow.Activities, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </Services>
   </WorkflowRuntime>
</configuration>

The CommonParameters section is nice because it allows us to share configuration for elements that have the same configuration.  In our case we are sharing a database for Workflow tracking and persistence.  You will find the SQL scripts at C:\windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN for the latest release anyhow.

The other services we have added here are the ManualWorkflowSchedulerService and the ExternalDataExchangeService.  The ManualWorkflowSchedulerService was created specifically for ASP.NET.  It requires us to do a little more management because we have to explicitly run the workflow when we need it.  This will be a non-issue once we add the PageFlowManager to our pages, but nevertheless it is required.  What this service does is it allows the runtime to borrow the current thread from ASP.NET itself.  That is important to understand as it will have affect on security and page postbacks.  A separate thread is automatically managed by the runtime for delay activities, but otherwise ASP.NET hosted workflows run in the same thread as the hosting application.

Just today I read that the ExternalDataExchangeService should be unique for each workflow instance.  I need to do a little more research on this one and I may amend this post if that is indeed true.  This service is used for communication between the host and the workflow.  We will use it with a simple data pipe service in order to simplify sending objects back and forth between the host and the workflow.  Of course the complexties involded will be wrapped up into the PageFlowManager and some workflow activities to make our jobs much easier.

In a future post I will examine the PageFlowManager and how it keeps the pages and the workflow in sync driving navigation and abstracting the management of the runtime from the application developer.

6/26/2006 11:17:13 PM (Mountain Daylight Time, UTC-06:00)  #    Disclaimer  |  Comments [1]  |  Trackback