State Your Destination Posts

My thanks go out to Mitchel Sellers for informing me that DotNetNuke has a bulk install feature. I had a need to modify an existing Windows service so it could silently update some DNN modules. I was able to make it work in this same service a couple years ago by modifying the DNN 4.3 source code to allow the main DotNetNuke.dll library to function outside of a web application (no easy feat, that). From there I could use the PaInstaller class to load a module. But then it wouldn’t work against 4.9.0 portals.

When I tried to do the same thing with the 4.9.0 source, it failed miserably. It just wouldn’t create an instance of SqlDataProvider, no matter how much I coaxed it. The better way was to have my service download the new module files (zips) to the install/module folder under the portal’s root, create an HttpWebRequest to http://mysite/install/install.aspx?mode=installresources, then parse the response to check for success.

The response parsing was easier than I thought it would be. I did the bulk update first using a web browser so I could see what I was dealing with. The HTML itself was nice in that it put non-blank space elements between each module name and result, so I could use Split() to break everything out. I ignored the first two elements of the resultant array since it contained all the unimportant stuff before the list of modules, then simply looped over the array and verified the word Success was in each even-numbered element, like so:

Dim aResults() As String

aResults = Split(response, " ")

If aResults.Length > 0 Then
    For i As Integer = 2 To aResults.Length - 1
        If i Mod 2 = 0 Then
            If Not aResults(i).Contains("Success") Then
                Return False
            End If
        End If
    Next i

    Return True
End If

I’m not sure why I had never heard of this feature over the course of developing several custom DNN modules, even though I’ve spent untold hours sifting through the forums at dotnetnuke.com and the Internet at large for detailed information on the portal’s architecture. In any case, that was the one new thing I learned that day.

A recent project of mine required querying Active Directory for all the groups in which a given user is a member, or if there isn’t a domain then searching the local machine. It also had to return all groups defined in a given domain. In the course of putting the code together, I created a library for future projects that needed to perform such queries. I recently made a major improvement to the code that retrieves a user’s AD groups and thought it would be good to share what I have so far.

It’s based on some bits of code I found on the web. Initially GetADGroupMembership would only return the first-level Active Directory groups the user was in, but that’s no good if the user is in a group that is in another group that is in yet another group that’s the one you really care about. So I added some recursion to bring up the entire group chain. It seems to perform well enough with our domain, which admittedly doesn’t have a great deal of nesting.

Imports System.DirectoryServices

Public Class LDAP
    Public Shared Function GetADGroupMembership(ByVal ldapPath As String, ByVal userName As String) 
                                                As List(Of String)
        Dim lstResults As List(Of String)
        Dim lstGroups As List(Of String)
        Dim lstSubGroups As List(Of String)
        Dim filter As String

        GetADGroupMembership = Nothing

        filter = "(&(objectClass=user)(samAccountName=" & userName & "))"
        lstResults = RunSearch(ldapPath, filter, "MemberOf")
        lstGroups = New List(Of String)

        If lstResults IsNot Nothing Then
            For Each result As String In lstResults
                lstGroups.Add(result)
                lstSubGroups = GetADGroupMembershipByGroup(ldapPath, result)

                For Each subGroup As String In lstSubGroups
                    If Not lstGroups.Contains(subGroup) Then
                        lstGroups.Add(subGroup)
                    End If
                Next subGroup
            Next result
        End If

        Return lstGroups
    End Function

    Public Shared Function GetLocalGroupMembership(ByVal ldapPath As String, ByVal userName As String,
                                                   ByVal password As String) As List(Of String)
        Dim de As DirectoryEntry = Nothing
        Dim colGroups As Object
        Dim lstGroups As List(Of String)

        GetLocalGroupMembership = Nothing

        Try
            de = New DirectoryEntry(ldapPath, userName, password, AuthenticationTypes.Secure)
            colGroups = de.Invoke("Groups")
            lstGroups = New List(Of String)

            For Each o As Object In colGroups
                lstGroups.Add(o.Name)
            Next o

            Return lstGroups
        Catch
            Throw
        Finally
            If de IsNot Nothing Then
                de.Dispose()
            End If
        End Try
    End Function

    Public Shared Function GetADGroups(ByVal ldapPath As String) As List(Of String)
        Dim lstGroups As List(Of String)
        Dim filter As String

        GetADGroups = Nothing

        filter = "(&(objectClass=group))"
        lstGroups = RunSearch(ldapPath, filter)
        lstGroups.Sort()

        Return lstGroups
    End Function

    Private Shared Function GetADGroupMembershipByGroup(ByVal ldapPath As String, ByVal groupName As String) 
                                                        As List(Of String)
        Dim lstResults As List(Of String)
        Dim lstGroups As List(Of String)
        Dim lstSubGroups As List(Of String)
        Dim filter As String

        GetADGroupMembershipByGroup = Nothing

        filter = "(&(objectCategory=group)(cn=" & groupName & "))"
        lstResults = RunSearch(ldapPath, filter, "MemberOf"
        lstGroups = New List(Of String)

        If lstResults IsNot Nothing Then
            For Each result As String In lstResults
                lstGroups.Add(result)

                'Retrieve all groups that the current group is a member of
                lstSubGroups = GetADGroupMembershipByGroup(ldapPath, result)
                For Each subGroup As String In lstSubGroups
                    If Not lstGroups.Contains(subGroup) Then
                        lstGroups.Add(subGroup)
                    End If
                Next subGroup
            Next result
        End If

        Return lstGroups
    End Function

    Private Shared Function RunSearch(ByVal ldapPath As String, ByVal filter As String,
                                      Optional ByVal propertyName As String = "") As List(Of String)
        Dim lstResults As List(Of String)
        Dim de As System.DirectoryServices.DirectoryEntry = Nothing
        Dim deSearcher As System.DirectoryServices.DirectorySearcher = Nothing
        Dim results As System.DirectoryServices.SearchResultCollection
        Dim res As System.DirectoryServices.SearchResult

        RunSearch = Nothing

        Try
            de = New System.DirectoryServices.DirectoryEntry(ldapPath)
            deSearcher = New System.DirectoryServices.DirectorySearcher(de)
            deSearcher.Filter = filter
            deSearcher.SearchScope = SearchScope.Subtree
            results = deSearcher.FindAll

            lstResults = New List(Of String)

            For Each res In results
                If propertyName = "" Then
                    'If no specific property is being sought, simply return the common name
                    lstResults.Add(TrimToName(res.GetDirectoryEntry.Name))
                Else
                    For Each o As Object In res.Properties(propertyName)
                        lstResults.Add(TrimToName(o))
                    Next o
                End If
            Next res

            Return lstResults
        Catch
            Throw
        Finally
            If deSearcher IsNot Nothing Then
                deSearcher.Dispose()
            End If

            If de IsNot Nothing Then
                de.Dispose()
            End If
        End Try
    End Function

    Private Shared Function TrimToName(ByVal path As String) As String
        Dim parts() As String

        parts = path.Split(",")
        Return parts(0).Replace("CN=", String.Empty)
    End Function
End Class

Update:
This library has grown and improved quite a bit over time, and I recently re-wrote it in C# and posted it on GitHub.

We have several new training servers where I work running Windows 2008 and SQL Server 2008. They are the only production machines we have running SQL 2008, and though they work fine, my experience with the latest version of SQL Server has made me wonder why anyone would be eager to upgrade from 2005.

I don’t know what major new features went into SQL 2008, but I do know one big thing that changed in Management Studio and it’s not good. I present exhibit A: the activity monitor. Previous versions of SQL Server had a place where you could see all the processes running on the server. It was handy because you could quickly tell who had locks on what database, find out if there were any processes that were blocking or being blocked, and you could end a process that couldn’t be ended through natural means. Yet somehow Microsoft decided that was all too simple.

First they moved it out of the object explorer and into the context menu of the database server itself, where nobody would think to look. I had to turn to Google to determine how you open it. Then once you start it you’re greeted with one of the worst user interfaces I’ve ever seen. You have to click on the Processes pane to show processes. All the columns of information are in one fixed-width grid with no horizontal scroll bar, so if you want to widen one it will come at the expense of narrowing another. And there are 16 columns! They have filter options in the column headers that dynamically allow you to filter by values in the column, which is all well and good, but the headers are more clunky than anything else. And finally, the grid defaults to only using about a third the vertical space of the window, and that’s all you get.

In short, it’s horrible and evil. I’m amazed that they would take something that was so easy to use and make it so unpleasant. Maybe I haven’t spent enough time with it,and maybe it’s due to the fact that we have the Express version, but if it’s really no more than what I’ve seen, I pray there will be some other tools out there that can show you the same information in a much better package.

A while back I wrote an ASP.NET user control that FTPs files to a server. It has a FileUpload control that allows the user to select the file they want to transfer, plus it uses some AJAX stuff to display a running total of bytes that have been sent. And it works great. I can throw it on an aspx page and upload to my heart’s content. But if I load it inside a custom web part within SharePoint, it tragically fails 100% of the time.

My control uses a third-party FTP library that supports asynchronous uploads via a BeginUpload method. That’s what I’m using in order to periodically refresh part of the page with the byte count. But for reasons unknown, when this code executes inside the SharePoint environment, the file stream from the FileUpload control gets unexpectedly closed after 64 KB have been transferred. As expected, the FTP library reacts badly when this happens. The crazy part is I could get it to work in SharePoint if I removed any one piece of the equation: if I used the synchronous upload method of the FTP library instead of the async one, or if I opened another thread but did anything else in it except try to FTP a file (counting to 20 on the page worked fine). I originally had the FileUpload control inside an UpdatePanel, so I took it out. Same result. (I know that FileUpload isn’t strictly supported inside update panels, but I had the appropriate trigger set and it was working beforehand anyhow.)

My assumption is that SharePoint was disposing the FileUpload control even though it should have been available during the subsequent async postbacks. Maybe it’s all part of the incompatibility between FileUpload and AJAX. In any case, the solution was to simply save the file on the web server via Request.Files(0).SaveAs, then FTP it. I wasn’t happy about inducing an additional delay, but in testing it wasn’t terrible.  I also save the relevant properties of the FileUpload control, like the file path, content type and content length, before I start the FTP transfer so I don’t have to touch the control at all while the transfer is happening.

So far it’s working swimmingly, but SharePoint is not endearing itself to my heart.

My employer has two web sites: our main one that contains mostly marketing materials, and the support site, which customers must log into and which allows them to download our new software releases, get documentation, and do various other support-related activities. The main site is fairly current in terms of layout and organization, however, the support site is downright ancient. We’ve been planning on unveiling a new one for some time now. The current plan is to build it around Microsoft SharePoint.

I knew very little about SharePoint when I was tasked a week and a half ago to develop a web part that can be used to manage file downloads. I’ve been spending the time reading up on the technology and figuring out the best way of creating a custom web part that will do what we need it to do. So far the results have been both promising and a bit frustrating.

SharePoint itself is interesting, in a ‘wow, you sure can set up a lot of layout and content using just a browser’ kind of way. It also seems like there are 5 different ways to do everything, which is a common trait of Microsoft software. My biggest sore spot is how things are geared towards doing all the development work on the server. I initially though I could set up a server but then do all the coding on my regular XP machine. Sadly, that didn’t fly. I had to load Visual Studio on the virtual Windows 2003 machine I had set up. It all works fine, but it’s a hassle.

My download web part will be based on some existing ASP.NET code I wrote that never went into production. That code features a grid control that shows links with the exact layout and behavior I wanted. I was fearful that I’d have to somehow output all the HTML rendered by the grid, until I realized I can incorporate user controls into a web part. That discovery lead into a whole different set of options: build a web part that loads user controls, or use SmartPart, a web part written by Jan Tielens that makes hosting user controls quick and easy.

I’ve decided to go with the first approach, which is to skip SmartPart and basically mimic its behavior with my own web part. My reasons were twofold: I was persuaded by articles I read online about the pros and cons of SmartPart, and by the fact that it will give me a chance to really learn the mechanics of writing a custom web part, not to mention allow complete control over everything. At this point I’m trying to figure out the ideal way to pass data from SharePoint to my web part, and then from the web part to my existing user control. The struggle contrinues…

I’ve never been a fan of Internet Explorer. I used Netscape starting with version 2.0 and only switched when Firefox came around. So I’ve never bothered to upgrade my XP development machine to IE 7. Then I read this post from Scott Hanselman about how a sizable chunk of his visitors are still running IE 6. He linked to a web site that’s actually devoted to getting people to upgrade. I’ve heard all the stories about IE 6’s particular horribleness, such as poor CSS support and a generous helping of security holes. But I didn’t care.

Except Scott somehow made me care. So I upgraded my machine and I must say, IE 7 loads pretty fast, way faster than Firefox 3. I suppose it’s good to stay current, but when Microsoft feels it has to change practically everything with each new release of software, it makes it hard to really want to upgrade. I dread the day I’ll have to move to an OS beyond XP. Maybe Windows 7 won’t be as bad as Vista, but I’m not going to hold my breath.

You would think spell checking a text box on a web form would be a snap. Firefox manages to do it the background: simple, easy, and it always works. But Telerik decided to go a different way, the ridiculously hard way.

I’ve noticed that the more complex controls in Telerik’s ASP.NET suite actually render as three or four separate HTML elements. Typically one is visible and the others are hidden. It makes sense to do it this way if you need to store several disparate pieces of information, or you need to do a lot of juggling on the client to implement some magic piece of functionality. But it means updating the actual content is trickier. The RadSpell control works this way.

What I wanted to do was automatically update some text after the user initiated and completed a spell check. A Telerik knowledge base article suggested manually updating the text box from their RadSpell control’s client-side OnClientDialogClosing event handler. The problem there was that event doesn’t seem to exist. There’s an OnClientDialogClosed event, which I thought might be good enough, but no dice. I couldn’t get the text to update.

I dug a little deeper and found an alternate approach, one that would give the RadSpell control the ClientID of the text box to update so it could do it for you. That didn’t work out so well either. It would update the text but as soon as I clicked in the text box it would revert to the misspelled text.

After fiddling with it for the better part of an afternoon, I got it to work in both IE and Firefox using OnClientDialogClosed. I don’t know what I was missing before, but it now correctly updates the user’s text. Afterwards I had to wonder if there wasn’t a better way. Are today’s super-duper whiz-bang third-party controls just so complex that the simple things become the hardest?

Telerik recently managed to brighten my day. I’ve been using their WinForm and ASP.NET suites for a few weeks now, long enough to realize there are several things about them that vex me. Case in point: the text boxes.

I have an application with a web front end and a small WinForm back end that will be used by administrators. I wrote the admin module first, and it needed to allow the user to input and edit currency values. The WinForm suite only has one masked text box control, so that’s what I went with. But it isn’t quite as nice as I hoped, probably because it has to be all things to all people. It treats its values more like text than numbers. If you assign a numerical value to it that is not as wide as the overall mask, it won’t right-justify the text, which for dollar amounts is very wrong. So I had to prefix shorter values with zeroes. I could set the text so the placeholder character is shown rather than the zeroes, but that falls apart as soon as the user touches the control, even just by tabbing over it. The zeroes pop right up. It’s very minor, obviously, but you’d think that after paying a thousand bucks for super snazzy controls they would be a little smarter.

I had a similar need in the web module for editing currency values. Here’s where Telerik started to redeem itself. The ASP.NET suite has a numeric text box, as well as the regular masked text box. I didn’t notice the numeric one at first. My eyes kind of glazed over looking through the dozens and dozens of controls in the toolbox. I saw the regular masked text box and figured since it was in the other suite, that it was all they had.

The numeric text box is perfect. It allows free-form input like any text box and then beautifully formats the value into the type of number you want. Now if they could only port that control to the WinForm package…

The DotNetNuke Client API has always seemed mysterious and confusing to me, so much so that I phased it out of a module I wrote a while back in favor of an AJAX web service. But there are certain parts that are useful, namely the ability to store values on the server side for use on the client side, and vice-versa. I recently ran into an issue using it that fortunately was simple to fix. I have a module that will be accessed anonymously, with no user login at all. I was having trouble using the DNN namespace in client script because the browser said it couldn’t find it, which didn’t make any sense at all. Somewhere during the login process all the namespaces get exposed, because once I logged in things worked as expected.

The key was calling DotNetNuke.UI.Utilities.ClientAPI.RegisterClientReference in Page_Load of the page where I was trying to use the API. The API documentation mentions that routine, but the context was such that I only expected to need to call it if I was using a version of DNN prior to 4.0 (I had 4.9.0) In any case, it’s fixed and the module development continues apace. My thanks go to Jon Henning, creator of the API, for the tip.