CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Eric Wise

Business & .NET

March 2005 - Posts

  • IsEmailAddress

    Short, sweet, and to the point.

        Protected Function IsEmailAddress(ByVal Address As String) As Boolean
            Return System.Text.RegularExpressions.Regex.IsMatch(Address, "^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$")
        End Function
  • Telerik Treeview Control

    As I had previously posted, the company I'm working for right now has purchased 5 subscriptions to the Telerik Control Suite.  I've had some queries from interested developers asking how I like the controls so far and the answer is that I like them a lot!

    Today I'm going to show a brief example of how we are using the treeview control to handle email contacts.  We have an internal only custom mail system that allows users to keep track of contacts and organize them into folders.  Being that folders and contacts are have hierarchical relationships this is the perfect use for the telerik treeview control.  We also wanted to get rid of our old clunky interface and use context menus to give managing contacts a "desktop" feel on the web.

    Step 1: HTML view

    Here is the html attributes of the treeview control for reference.  You can see how various events and paths have been set for images, context menus, etc.

    id="tvwMailContacts" runat="server" OnNodeEdit="HandleNodeEdit" AllowNodeEditing="True" BeforeClientContextClick="ContextMenuClick" ContextMenuContentFile="~/XML/ContextMenus.xml" OnNodeContextClick="HandleContextClick" ImagesBaseDir="~/Images/Outlook/" CausesValidation="False"

    Step 2: Setting up the context menus

    Every node on the Telerik tree can be assigned to a context menu XML file, the path of which is set in the properties.  Every user may add or delete contacts and folders owned by them, but there are some system level folders that they are not allowed to edit or delete.  So we ended up with three types of context menus defined in xml as follows:

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

    <ContextMenus>

       <Menu Name="UserContactFolder">

          <Item Image="drafts.gif" Text="Edit Folder" PostBack="False" />

          <Item Image="newFolder.gif" Text="New Folder" PostBack="True"/>

          <Item Image="contact.gif" Text="New Contact" PostBack="True"/>

          <Item Image="deleteMessage.gif" Text="Delete Folder" PostBack="True"/>

       </Menu>

       <Menu Name="SystemContactFolder">

          <Item Image="newFolder.gif" Text="New Folder" PostBack="True"/>

          <Item Image="contact.gif" Text="New Contact" PostBack="True"/>

       </Menu>

       <Menu Name="UserContact">

          <Item Image="drafts.gif" Text="Edit Contact" PostBack="True" />

          <Item Image="deleteMessage.gif" Text="Delete Contact" PostBack="True"/>

       </Menu>

    </ContextMenus>

    Notice how ourthree menu types expose different commands and you can even control whether the click causes a postback or not.  Editing a contact causes a postback because there is a few fields for contact information while editing a folder name can happen on the client before it posts back.  We'll get into the code for that later.

    Step 3: Populating the treeview

    To create the propery hierarchy for the treeview control, we are pulling two DataTables into a dataset, defining a relationship and using the GetChildRows dataset method.  The code to do this is as follows:

    First, we walk the rows in the folder table and find the rows where the parentFolder is null, that means they are top level folders.  Once we find a top level folder we create a node and then recursively populate its children, both folders and contacts.

        1         ds.Relations.Add("FolderRelation", ds.Tables(0).Columns("contactFolderID"), ds.Tables(0).Columns("parentFolder"), False)
        2         ds.Relations.Add("ContactRelation", ds.Tables(0).Columns("contactFolderID"), ds.Tables(1).Columns("contactParentFolder"), False)
        3  
        4         Dim dbRow As DataRow
        5         For Each dbRow In ds.Tables(0).Rows
        6             If dbRow.IsNull("parentFolder") Then
        7                 'populate node properties
        8                 Dim node As RadTreeNode = CreateContactNode(dbRow("contactFolderID"), dbRow("FolderName").ToString(), dbRow("folderImage").ToString(), True)
        9  
       10                 'Check to see it this is a user or system folder, or a contact
       11                 If IsDBNull(dbRow("folderOwner")) Then
       12                     node.ContextMenuName = "SystemContactFolder"
       13                 Else
       14                     node.ContextMenuName = "UserContactFolder"
       15                 End If
       16  
       17                 tree.AddNode(node)
       18                 RecursivelyPopulateContactFolders(dbRow, node)
       19                 RecursivelyPopulateContacts(dbRow, node)
       20             End If
       21         Next dbRow
    

    Here is the node creation and recursive logic.  You can see how the context menu name is set matching the XML file:

        1     Private Sub RecursivelyPopulateContactFolders(ByVal dbRow As DataRow, ByVal node As RadTreeNode)
        2         Dim childRow As DataRow
        3         For Each childRow In dbRow.GetChildRows("FolderRelation")
        4             'populate node properties
        5             Dim childNode As RadTreeNode = CreateContactNode(childRow("contactFolderID"), childRow("FolderName").ToString(), childRow("folderImage").ToString(), True)
        6  
        7             'Null owners are system folders
        8             If IsDBNull(childRow("folderOwner")) Then
        9                 childNode.ContextMenuName = "SystemContactFolder"
       10             Else
       11                 childNode.ContextMenuName = "UserContactFolder"
       12             End If
       13  
       14             node.AddNode(childNode)
       15             RecursivelyPopulateContactFolders(childRow, childNode)
       16             RecursivelyPopulateContacts(childRow, childNode)
       17         Next childRow
       18     End Sub
       19  
       20     Private Sub RecursivelyPopulateContacts(ByVal dbRow As DataRow, ByVal node As RadTreeNode)
       21         Dim childRow As DataRow
       22         For Each childRow In dbRow.GetChildRows("ContactRelation")
       23             'populate node properties
       24             Dim childNode As RadTreeNode = CreateContactNode(childRow("emailContactID"), childRow("Alias").ToString(), "contact.gif", False)
       25             childNode.ContextMenuName = "UserContact"
       26  
       27             node.AddNode(childNode)
       28         Next childRow
       29     End Sub
       30  
       31     Private Function CreateContactNode(ByVal ID As Integer, ByVal displayText As String, ByVal nodeImage As String, ByVal expanded As Boolean, Optional ByVal contextMenuName As String = "") As RadTreeNode
       32         Dim node As New RadTreeNode(displayText)
       33         node.Image = nodeImage
       34         node.Expanded = True
       35         node.ID = ID.ToString()
       36         Return node
       37     End Function
    

    The relational data looks like this:

    Step 4: Testing Context Menus

    We can now fire up the application and use the context menus for the 3 types of items (contact, system folder, and user folder).  Here's a shot of it in action:

    Step 5: Client Side Editing

    Notice from the telerik control html that we defined a javascript function for to fire on context clicks.  We want to capture the edit folder click and allow them to do a client side edit and then post the edit to the server.  All we have to do is define a javascript function (specified in the telerik html).

    function ContextMenuClick(node, itemText) {

       if (itemText == "Edit Folder") {

          node.StartEdit();

       }

       return true;

    }

    Here's the result:

    Step 6: Server Side Code

    All of the save/delete/update logic resides on the server in the subroutine specified in the telerik control html.  Here is the code to handle clicks based on their context menu type.  We have one for the postback menu items and one for the edit item.  This is very easy and confortable to do because the NodeEvents argument behaves very similarily to an item command:

        1     Protected Sub HandleContextClick(ByVal sender As Object, ByVal NodeEvents As RadTreeNodeEventArgs)
        2         Try
        3             Dim contextCommand As String = NodeEvents.ContextMenuItemText
        4             Dim currentNode As RadTreeNode = NodeEvents.NodeClicked
        5  
        6             Dim contactFolder As New SchoolOne.Mail.EmailContactFolder
        7             Select Case contextCommand
        8                 Case "Delete Folder"
        9                     If NodeEvents.NodeClicked.Nodes.Count > 0 Then
       10                         Throw New Exception("Can not delete a folder with items in it!")
       11                     End If
       12                     contactFolder.ID = Integer.Parse(NodeEvents.NodeClicked.ID)
       13                     contactFolder = MailDomainManager.Load(contactFolder)
       14                     MailDomainManager.Delete(contactFolder)
       15  
       16                 Case "New Folder"
       17                     contactFolder.Owner = ThisUser.InternalEmail
       18                     contactFolder.Name = "New Folder"
       19                     contactFolder.ParentFolder = Integer.Parse(NodeEvents.NodeClicked.ID)
       20                     contactFolder.Image = "folder.gif"
       21                     contactFolder = MailDomainManager.Save(contactFolder)
       22  
       23                 Case "New Contact"
       24                     ShowControls()
       25                     ParentFolderID.Text = NodeEvents.NodeClicked.ID
       26  
       27                 Case "Edit Contact"
       28                     ShowControls()
       29                     EmailContactID.Text = NodeEvents.NodeClicked.ID
       30                     Dim emailContact As New SchoolOne.Mail.EmailContact(Integer.Parse(EmailContactID.Text))
       31                     emailcontact = MailDomainManager.Load(emailcontact)
       32                     txtContactEmail.Text = emailcontact.EmailAddress
       33                     txtAlias.Text = emailcontact.ContactAlias
       34  
       35                 Case "Delete Contact"
       36                     Dim emailContact As New SchoolOne.Mail.EmailContact(Integer.Parse(NodeEvents.NodeClicked.ID))
       37                     MailDomainManager.Delete(emailContact)
       38             End Select
       39  
       40             LoadContacts(tvwMailContacts)
       41         Catch ex As Exception
       42             WriteException(ex, ExceptionLog.None)
       43         End Try
       44  
       45     End Sub
       46  
       47     Protected Sub HandleNodeEdit(ByVal sender As Object, ByVal NodeEvents As RadTreeNodeEventArgs)
       48         'check if it is a folder or a contact
       49         Try
       50             If NodeEvents.NodeEdited.Image = "folder.gif" Then
       51                 Dim contactFolder As New SchoolOne.Mail.EmailContactFolder
       52                 contactFolder.ID = Integer.Parse(NodeEvents.NodeEdited.ID)
       53                 contactFolder = MailDomainManager.Load(contactFolder)
       54                 contactFolder.Name = NodeEvents.NewText
       55  
       56                 contactFolder = MailDomainManager.Save(contactFolder)
       57             End If
       58  
       59             LoadContacts(tvwMailContacts)
       60         Catch ex As Exception
       61             WriteException(ex, ExceptionLog.None)
       62         End Try
       63     End Sub
    

     

    Conclusion

    So far I am very impressed with the ease of configuring and coding in the Treeview control.  I will post more feedback as I get into the next controls: Editor, Spellcheck, and PanelBar.

  • ASP .NET Forms Authentication Password Hashes- Level 200

    A common question I get from ASP .NET developers is how to quickly and easily set up forms authentication where the passwords in the database are not stored in plain text.  It actually shocks me how many systems I've gone into on the database end and seen passwords for every account in the application sitting there in clear text.  Here's how Easy Assets .NET handles hashing passwords with a random salt value.

    First let's take a look at our LoginUser object:

        1 Imports System.Web.Security
        2 Imports System.Security.Cryptography
        3  
        4 Public Class LoginUsers
        5     Inherits DomainObjects
        6  
        7     Public Overloads Sub Add(ByVal newItem As AcquisitionMethod)
        8         MyBase.List.Add(newItem)
        9     End Sub
       10  
       11     Default Public Shadows Property Item(ByVal index As Integer) As AcquisitionMethod
       12         Get
       13             Return CType(list(index), AcquisitionMethod)
       14         End Get
       15         Set(ByVal Value As AcquisitionMethod)
       16             list(index) = Value
       17         End Set
       18     End Property
       19 End Class
       20  
       21 Public Class LoginUser
       22     Inherits DomainObject
       23  
       24 #Region "Constructors"
       25     Public Sub New()
       26         Me.UserName = ""
       27     End Sub
       28  
       29     Public Sub New(ByVal LoginName As String)
       30         Me.UserName = LoginName
       31     End Sub
       32 #End Region
       33  
       34 #Region "Private Members"
       35     Private _username As String
       36     Private _emailaddress As String
       37     Private _firstname As String
       38     Private _issuesolver As Boolean
       39     Private _lastname As String
       40     Private _password As String
       41     Private _roles As System.Text.StringBuilder
       42     Private _salt As String
       43     Private _isActive As Boolean
       44 #End Region
       45  
       46 #Region "Properties"
       47     Public Property EmailAddress() As String
       48         Get
       49             Return Me._emailaddress
       50         End Get
       51         Set(ByVal value As String)
       52             Me._emailaddress = value
       53         End Set
       54     End Property
       55  
       56     Public ReadOnly Property FirstLast() As String
       57         Get
       58             Return (Me._firstname & " " & Me._lastname)
       59         End Get
       60     End Property
       61  
       62     Public Property FirstName() As String
       63         Get
       64             Return Me._firstname
       65         End Get
       66         Set(ByVal value As String)
       67             Me._firstname = value
       68         End Set
       69     End Property
       70  
       71     Public Property IssueSolver() As Boolean
       72         Get
       73             Return Me._issuesolver
       74         End Get
       75         Set(ByVal value As Boolean)
       76             Me._issuesolver = value
       77         End Set
       78     End Property
       79  
       80     Public ReadOnly Property LastFirst() As String
       81         Get
       82             If ((Not Me._lastname Is String.Empty) AndAlso (Not Me._firstname Is String.Empty)) Then
       83                 Return (Me._lastname & ", " & Me._firstname)
       84             End If
       85             Return ""
       86         End Get
       87     End Property
       88  
       89     Public Property LastName() As String
       90         Get
       91             Return Me._lastname
       92         End Get
       93         Set(ByVal value As String)
       94             Me._lastname = value
       95         End Set
       96     End Property
       97  
       98     Public Property Password() As String
       99         Get
      100             Return Me._password
      101         End Get
      102         Set(ByVal Value As String)
      103             Me._password = Value
      104         End Set
      105     End Property
      106  
      107     Public Property salt() As String
      108         Get
      109             Return Me._salt
      110         End Get
      111         Set(ByVal Value As String)
      112             Me._salt = Value
      113         End Set
      114     End Property
      115  
      116     Public ReadOnly Property Roles() As String
      117         Get
      118             If IsNothing(_roles) Then
      119                 Me._roles = New System.Text.StringBuilder
      120  
      121                 Dim query As New EasyAssets.DAC.UserRoleQuery("RoleDescription")
      122                 query.UserRole = New EasyAssets.DAC.UserRole
      123                 query.UserRole.UserName = Me._username
      124  
      125                 Dim dt As DataTable = ObjectDomainMGR.ListSummary(query)
      126                 For Each dr As DataRow In dt.Rows
      127                     Me._roles.Append(dr("RoleName") & "|")
      128                 Next
      129             End If
      130  
      131             Return Me._roles.ToString()
      132         End Get
      133     End Property
      134  
      135     Public Property UserName() As String
      136         Get
      137             Return Me._username
      138         End Get
      139         Set(ByVal value As String)
      140             Me._username = value
      141             Me.Key = value
      142         End Set
      143     End Property
      144  
      145     Public Property isActive() As Boolean
      146         Get
      147             Return _isActive
      148         End Get
      149         Set(ByVal Value As Boolean)
      150             _isActive = Value
      151         End Set
      152     End Property
      153 #End Region
      154  
      155 #Region "Methods"
      156     Private Sub CreatePasswordHash()
      157         Dim passwordSalt As String = (_password & _salt)
      158         _password = FormsAuthentication.HashPasswordForStoringInConfigFile(passwordSalt, "SHA1")
      159     End Sub
      160  
      161     Private Sub CreateSalt(ByVal size As Integer)
      162         Dim provider1 As New RNGCryptoServiceProvider
      163         Dim buffer1 As Byte() = New Byte(size - 1) {}
      164  
      165         provider1.GetBytes(buffer1)
      166         _salt = Convert.ToBase64String(buffer1)
      167     End Sub
      168  
      169     Public Sub SetPassword()
      170         CreateSalt(5)
      171         CreatePasswordHash()
      172     End Sub
      173 #End Region

    Creating/Changing a Password

    When you load the loginuser object from the database the password hash and the salt value are stored in the appropriate properties.  If you are creating a new user or wish to change the password of an existing user you simply set the Password property to the plain text value of the password from your form.  Then you call the SetPassword() method which generates a random salt value and uses it in conjunction with the plain text password to create the hash.

    Logging In

    So how do we authenticate the user then?  The code is fairly simple.  The Login.aspx page has a username and password textboxes and a button to click.  The button code grabs the LoginUser from the database specified by the username and if the user is active uses the salt value to generate a hash for comparison.  The code looks like this:

        Private Sub btnLogin_Click(ByVal sender As System.Object, ByVal e As System.Web.UI.ImageClickEventArgs) Handles btnLogin.Click
            Try
                Dim thisUser As New EasyAssets.DAC.LoginUser(txtUserName.Text)
     
                thisUser = DomainManager.Load(thisUser)
     
                If thisUser.isActive Then
                    Dim passwordAndSalt As String = String.Concat(txtPassword.Text, thisUser.salt)
                    Dim hashedPasswordAndSalt As String = FormsAuthentication.HashPasswordForStoringInConfigFile(passwordAndSalt, "SHA1")
     
                    If Not thisUser.Password.Equals(hashedPasswordAndSalt) Then
                        Throw New System.Exception("Invalid Username/Password combination!")
                    End If
     
                    Dim authTicket As New FormsAuthenticationTicket(1, thisUser.UserName, DateTime.Now, DateTime.Now.AddMinutes(30), False, thisUser.Roles)
                    Dim encryptedTicket As String = FormsAuthentication.Encrypt(authTicket)
     
                    Dim authCookie = New HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket)
                    Response.Cookies.Add(authCookie)
                Else
                    Throw New Exception("Account Expired!  Contact an administrator.")
                End If
     
                Response.Redirect("Welcome.aspx", False)
            Catch ex As Exception
                WriteMessage(ex.Message, True)
            End Try
        End Sub

    Role Based Security

    You'll notice that the authentication ticket being created passes in the LoginUser's Roles.  In EasyAssets .NET there are a list of roles that any user can be assigned to and they grant or deny access to various parts of the application.  You'll notice that the Roles() property of the LoginUser queries the database and then builds a StringBuilder with the role names delimited by the pipe |.  The end result is that if you have a role called "ADMIN" you can simply do context.user.identity.isInRole("ADMIN") to validate whether that user is the admin.

    How does ASP .NET handle building the ticket and setting the roles on the context.user?  Simply add the following code to your global.asax:

        Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
            Dim CookieName As String = FormsAuthentication.FormsCookieName
            Dim authCookie As HttpCookie = Context.Request.Cookies(CookieName)
     
            'Check for cookie
            If IsNothing(authCookie) Then Return
     
            Dim authTicket As FormsAuthenticationTicket
            Try
                authTicket = FormsAuthentication.Decrypt(authCookie.Value)
     
            Catch
                Return
            End Try
     
            If IsNothing(authTicket) Then
                'Cookie failed to decrypt
                Return
            End If
     
            Dim roles() As String = authTicket.UserData.Split("|")
            Dim id As New FormsIdentity(authTicket)
            Dim principal As New System.Security.Principal.GenericPrincipal(id, roles)
            Context.User = principal
        End Sub

    Finally, set your web.config to be forms authentication mode as follows:

    <authentication mode="Forms">

       <forms name="EasyAssetFormsAuth" loginUrl="WebModules/Security/LogIn.aspx" protection="Encryption" timeout="30" path="/">

       forms>

    authentication>

    <authorization>

       <deny users="?" />

       <allow users="*" />

    authorization>

  • Fixing the software development industry

    Darrell has posted his opinion on how to address some of the problems in the software industry today and if there is one thing I truly enjoy, it's being able to give my opinion!  I think that Darrell has nailed part of the problem, that salaries and value in general do not match up well for developers anywhere.

    To find a complete solution though we must understand why this is.  Surely there is "deadwood" as Darrell calls it in most organizations.  Obviously if the goal is to match value to salary the first thing that needs to be done is that we must find a good way to identify the dead wood and trim the branches.  How does one identify a poor developer on the job?

    Makes no attempts for self improvement

    When I walk into a developers area, the first thing I tend to look at is their bookshelf.  The variety and quality of books the developer owns can tell you a lot about what coding techniques they prefer, and in general show that they are at least trying to follow industry standards and keep their skillset up to date.  Now obviously not all developers keep a library at work, so how else can we identify developers that actively seek improvement?

    • Does the developer blog?
    • Are they a member of any local user groups?
    • Do they take classes or seek certifications?
    • If there is an education budget at your company, do they use it?

    I am a big fan of companies providing education benefits.  Companies should be aware that the better the education and training their staff has the more valuable they are as employees.

    Code is difficult to maintain

    This is something that takes time to measure.  When I am managing an IT project I like to be certain that multiple team members touch portions of an application.  Sure, some developers touch certain areas more than others to become specialists, but I feel it is important that a variety of developers get exposed to an area of the code.  Over time, as many developers review and have to make changes to areas of the code you get a feel for whose code requires the most work to maintain.  At this point you must evaluate why the code is difficult and time consuming to maintain.  It will fall into one of the following categories:

    • Poor specification- Here's a warning flag for poor planning and communication between the users/management and the developer/architect.
    • Complex Design- Sometimes an area of the code is just required to do some really complicated things and it's no one's fault.
    • Bad code- The rest of the time it's the fault of the developer.  Are they not following coding standards?  Do they have a fundamental misunderstanding of how to write clean code?  Are they a junior developer who is performing tasks for the first time?  All of these things can be fixed with time and mentoring, but if the developer is one of the above who doesn't seek self improvement then it is probably more difficult and time consuming than it is worthwhile.

    Poor personalities

    I know we have all experienced working with an IT professional who has a decent skillset, but is abrasive and arrogant.  In a pure software shop a developer can somewhat get away with this if they are highly skilled.  However (and I speak from a load of experience here) the business world has increasingly broken down the communication barrier between IT and the other departments in the company.  Many new design processes go so far as to say that end users should be members of the project team.  Regardless of whether you think this is a fad or something that will stick around, the bottom line is over the years of my experience I have become more directly involved with end users, not less.  If a developer has an abrassive personality they can do severe damage to the relationship between IT and other departments.  This sort of destructive activity can not be tolerated!

     

    Now that I've given you tips for identifying deadwood let's talk about the real work that needs to be done: preventing deadwood from getting into the organization in the first place.  Have held a mix of contract and perm positions over my career, I have gone through quite a few interviews.  For the most part, they suck.  That's right, they suck.  They are poorly planned, in general the questions I've been asked are either too general or way too specific, and I can count the number of times I've been asked to provide code samples on one hand.

    Interviewing a Developer

    Here are some general tips for improving your hiring process.  One of the common complaints I get when I talk to people about improving their hiring process is that it "takes time".  Well, as studies have shown, hiring the wrong people is far more expensive than letting your IT staff conduct proper interviews.  The interview will differ by the level of experience of the candidate, so here's some tips.

    Junior Developer

    This is a new graduate, or a developer with less than 2 years of experience.  Being that they don't have a lot of project experience, you're probably not going to be able to get much in the way of code samples out of them.  For junior developers I like to provide a workstation with visual studio .net, MSDN library, and a copy of the pubs database.  I task them with pulling a sortable list from one of the tables.  They can use any .NET language they want, forms or asp .net.  They have all the help files in MSDN and 45 minutes to figure it out if they don't already know how to do it.  This checks two things:

    1. If they already know how to do it then I can check their coding style and see which method they used so I can ask followup questions in the face to face interview later.
    2. If they don't know how to do it then I can make sure that they can actually read help files and implement solutions.

    #2 is very very important.  One of the biggest differences between a poor developer and a great one is that the great one knows how to research solutions.  A poor developer will spin their wheels, ask someone else, whine a lot, and generally be unproductive until someone comes and holds their hand.  As a mentor/team lead I absolutely hate it when a developer comes to me with a problem and they haven't even attempted to find the solution on their own.  You'll get far better results with me if you say "I'm trying to do X, I did some research and tried A, B, and C but none of them work.  Can you help me?".  As an organization you want to staff filled with people who are proactive about problem solving.

    Mid-Level Developer

    Mid-Level developers have between 2 and 5 years of experience and have done a lot more coding than the juniors.  For mid level developers I like to request a code sample (or in the case of an ASP .NET developer) I like to get urls of sites they have worked on if possible.  Some folks are bound by NDA agreements and can't provide this, but frankly if you are a mid level developer and you don't have any "play" code laying around somewhere then immediately you strike me as a person who doesn't seek self improvement!  (see how that ties back nicely above?)  I'm not looking for the most elegant example on the planet, you're Mid-Level which means you still have some growing to do.  What I do expect is to be able to see an example of your work, and brief explanations of how and why the work went about the way it did.  I want to see someone who understands what they have done and tell me why they would or would not do it the same way if asked to code it again.

    Basically, I just want to see if the mid-level developer is thinking or just doing.  If they are just doing, then you are basically paying a mid level salary for a junior skill level.

    Senior Developer

    The senior developer should be able to talk about their experiences in leading others.  Code samples are also helpful, but I feel they are less of a requirement for senior developers than they are for mid level developers but in the end it depends on how much actual coding your senior guys do in your organization.  In a perfect world the senior would spend a significant (30-50%) amount of time mentoring lower level developers, reviewing code, and helping determine the overall vision for the software.  I want to see that the senior developer is read up on recent trends in IT, not a person who is happy being stuck in older technologies and looking to retire somewhere in maintenance mode.  Basically I want to see if there is still passion about coding and technology there, oftentimes I find senior people to be burned out and rooted in their ways.  This is a potential harmful role model if they give this impression.

  • Pricing Easy Assets .NET

    I've reached a mini-milestone with the local install, source included version of Easy Assets .NET and now I have started thinking ahead to how I should price this software.  Pricing is perhaps the most difficult thing for an ISV to do.  On one hand you want to be fair and not overcharge for your product, on the other hand you do not want to price too low and lose a bunch of money either.

    I've read articles by Eric Sink, and for the most part I find his advice useful and thought provoking.  For those of you who have not read his Product Pricing Primer, I suggest you do it!

    Back to Easy Assets .NET.  I have to come up with a logical, fair, and profitable way to market and sell this product.  Looking at my competitors and options I have drawn the following conclusions:

    1. There are some very ugly, clunky Fixed Asset Management (FAM) programs out there that sell for thousands of dollars.  Some products I've found I can't believe how they are in business still.
    2. It seems that the range for FAM software of minimal quality standards starts around $500 and you can spend upwards of $20,000 if you're so inclined.  Web enabled solutions I haven't seen much under $1500.
    3. There is a push towards web enabling asset management software, so I'm ahead of the curve.
    4. I have not found any commercial source included FAM products.  I'm sure there has to be some out there somewhere, but they aren't easy to find.

    With all this in mind, I feel like I'm in a pretty good position here.  By offering my product source-included I fall into a very nice niche for companies that want to implement a FAM system and want to be able to customize it to meet their specific business needs.  The rather broad range of prices for FAM software means that I can be agressive on price and still be able to make a profit.

    Now we add the source included twist.  When a product such as mine is source included, there is a significant opportunity to sell maintenance agreements and consulting services to help your customers tailor the software to their needs.  Many open source companies like RedHat Linux make most of their money off of service agreements and almost no money off the software itself.  I think this a solid strategy if you are targeting enterprise (medium to large) customers.  Easy Assets .NET, however, is more targeted towards the small and medium businesses who can benefit from FAM but don't want to spend a fortune.

    Add to all this the substantial research by Gartner and others that show the cost savings that proper asset management can give your company.  I've seen statistics that show just for IT you can save upwards of $300 per user with proper license and asset tracking.  Avoiding illegal licensing is becoming increasingly important.  This is a great selling point for the software since I can point to lots of research showing cost savings that far exceed what I will charge.

    So how much will I charge?  I'm thinking somewhere in the $800-$1,200 range with yearly maintenance/service plans available.  If your company has expert coders, you won't need the maintenance/service.  It's more for companies that don't have a strong IT staff level.  All purchases will include unlimited email support and 1 year of free downloads of new versions of the source code.

    Time will tell if my gut instinct on this price level is correct!

  • Telerik Control Suite

    The company I am working for has purchased 5 subscription licenses for the Telerik control suite on my recommendation.

    I'm pleased to give an initial impression that the controls are stable, easy to work with, and cross browser compatible.  I'm quite impressed with their controls and would recommend them.

    If all you coders out there in blog reader land would like me to do a series on the controls with code examples I'd be happy to provide it, but I want it to be requested since they are 3rd party controls and the series probably wouldn't be of interest to many of you.  If there is enough demand I'll do it though.

  • Enterprise Library Registry Error (installservices.bat)

    So, I've recently finished Beta 1 of the local install of Easy Assets .NET and I started passing it to some of the beta testers (thanks to those companies that graciously accepted to test it).  So far a few people have been into my code and I've received many compliments on how clean and nice the database and code is.  I'm glad that my refactoring seems to have paid big dividends.

    Suddenly, mysteriously, I got two reports from beta testers that the application wasn't working as per my install instructions and was giving a mysterious registry error in the enterprise library.  How could this be?!  The Enterprise Library has worked fine for me in development for months.  I assembled the following information to start:

    • Problem occurs when deploying to Windows Server 2003
    • Registry error fires inside the database application block when logging tries to fire
    • Installing the enterprise library package from microsoft on the server does not fix the problem
    • Visual Studio is not installed on the server.

    This was all very odd so I threw open my command window and tried to execute the InstallServices batch file (installservices.bat).  It threw an error that visual studio was not installed.  I boggled, why would you need visual studio to install the enterprise library services?  Come to find out it was just looking for a utility provided by the .NET Framework.  I leapt into action (with some help from Brenden Tompkins) and I modified the InstallServices.bat file to go right to the framework directory and grab the needed utility.

    In addition, installing the enterprise library does not compile it without having visual studio installed.  The way I fixed this was to install the Enterprise Library then copy the /bin directory from another machine that I had successfully compiled the Enterprise Library on.  Then I replaced the installservices.bat file with my modified one and VOILA!  Errors gone and Easy Assets .NET works properly.

    Without further ado, here is the installer script to use to get services running without visual studio, my modifications are highlighted:


    @echo off
    @REM  ---------------------------------------------------------------------------------
    @REM  InstallServices.bat file
    @REM
    @REM  This batch file installs/uninstalls various services for the Enterprise Library
    @REM  application blocks.
    @REM 
    @REM  Optional arguments for this batch file:
    @REM    1 - /u to unstall. Otherwise it is installed.
    @REM  ----------------------------------------------------------------------------------

    echo.
    echo ==========================================================================
    echo   InstallServices.bat                                                   
    echo      Installs/uninstalls services for the Enterprise Library 
    echo ==========================================================================
    echo.

    set Frameworkdir=%SystemRoot%\Microsoft.NET\Framework\v1.1.4322\
    set binDir="..\bin"
    set pause=true

    @REM  ---------------------------------------------------------------
    @REM  User can override default directory containing the
    @REM  the Enterprise Library assemblies by supplying
    @REM  a parameter to batch file (e.g. InstallServices C:\bin).
    @REM  ---------------------------------------------------------------

    if "%1"=="/?" goto HELP

    if "%1"=="" goto RUN

    @REM  ----------------------------------------------------
    @REM  If the first parameter is /q, do not pause
    @REM  at the end of execution.
    @REM  ----------------------------------------------------

    if /i "%1"=="/q" (
     set pause=false
     SHIFT
    )

    @REM  ----------------------------------------------------
    @REM  If the first parameter is /u, uninstall.
    @REM  ----------------------------------------------------

    if /i "%1"=="/u" goto RUN

    goto HELP

    :RUN

    @REM  ------------------------------------------------
    @REM  Shorten the command prompt for making the output
    @REM  easier to read.
    @REM  ------------------------------------------------
    set savedPrompt=%prompt%
    set prompt=*$g

    @ECHO ----------------------------------------
    @ECHO InstallServices.bat Started
    @ECHO ----------------------------------------
    @ECHO.

    @REM -------------------------------------------------------
    @REM Change to the directory where the assemblies reside
    @REM -------------------------------------------------------

    pushd %binDir%

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the Common Application Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Common.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Common.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the Caching Application Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Caching.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Caching.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the ConfigurationApplication Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Configuration.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Configuration.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the Cryptography Application Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Security.Cryptography.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Security.Cryptography.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the Data Access Application Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Data.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Data.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------------
    @ECHO Installing Services for the Exception Handling Application Block
    @ECHO -----------------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO ---------------------------------------------------------------------------------
    @ECHO Installing Services for the Logging and Instrumentation Application Block
    @ECHO ---------------------------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Logging.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Logging.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO -----------------------------------------------------------------
    @ECHO Installing Services for the Security Application Block
    @ECHO -----------------------------------------------------------------
    @ECHO.

    if Exist Microsoft.Practices.EnterpriseLibrary.Security.dll %Frameworkdir%installutil %1 Microsoft.Practices.EnterpriseLibrary.Security.dll
    @if errorlevel 1 goto :error

    @ECHO.
    @ECHO ----------------------------------------
    @ECHO InstallServices.bat Completed
    @ECHO ----------------------------------------
    @ECHO.

    @REM  ----------------------------------------
    @REM  Restore the command prompt and exit
    @REM  ----------------------------------------
    @goto :exit

    @REM  -------------------------------------------
    @REM  Handle errors
    @REM
    @REM  Use the following after any call to exit
    @REM  and return an error code when errors occur
    @REM
    @REM  if errorlevel 1 goto :error 
    @REM  -------------------------------------------
    :error
    @ECHO An error occured in InstallServices.bat - %errorLevel%
    if %pause%==true PAUSE
    @exit errorLevel

    :HELP
    echo Usage: InstallServices.bat [/q] [/u]  
    echo.
    echo Examples:
    echo.
    echo    "InstallServices" - installs services for Enterprise Library assemblies      
    echo    "InstallServices /u" - uninstalls services for Enterprise Library assemblies
    echo    "InstallServices /q" - installs services, no pause when error occurs (quiet mode)    
    echo.

    @REM  ----------------------------------------
    @REM  The exit label
    @REM  ----------------------------------------
    :exit

    popd
    set pause=
    set binDir=
    set prompt=%savedPrompt%
    set savedPrompt=

    echo on


    I would wager that you don't need to install the enterprise library installer at all and could just xcopy the .dll's in with your project and point this script at your project's /bin directory.

  • Politics and Role Based Security

    I spent my afternoon yesterday justifying a role based security system for the application I'm heading up.  Why would I have to do such a thing?  Well, I had an encounter with one of the most fearsome beasts of the business world.  That beast is called:

    The DBA who thinks they are a programmer

    Now, let me say that I have nothing against the typical DBA.  In fact, I find the involvement of a professional DBA on a project to be a great help when it comes to normalization exercises, complex stored procedures, and triggers.  Being that I've worn both the DBA and programmer hats I am able to do these things myself, but frankly they aren't my favorite things in the world to spend time on so I'd just as soon pass these duties off to someone who specializes in it.

    So why is this non-typical DBA such a fearsome beast?  It is simply because they constantly attempt to involve themselves in the logic and structure of the programming needs rather then taking the requirements and building the database to best meet those requirements.  Case in point with the meeting yesterday.  The application we are building is going to have at minimum eight different security groups that users will need to be placed in.  Each of these groups is to have a mix and match of rights in the system.  In addition, group members who misbehave in certain areas of the software need to be able to have their rights to those areas revoked without losing access to the other areas they do have.

    A situation such as this is a perfect fit for role based security.  I proposed that we create security groups, assign roles to the groups, and assign users to those groups.  (We don't feel the need to go all the way down to assigning roles to individual users for various reasons I'm not going to go into.)  I mocked up the table structure we'd need to support this in Visio and approached the DBA with the design and met a hard line of resistance.  The conversation went something like this:

    DBA: You're adding like 6 tables to the database, that adds too much complexity.

    Me: That is the minimum number of tables it takes to properly implement role based security.

    DBA: You should use an access level system instead.

    Me: