Title:  Web Method To Display Server Directory Tree Remotely on Mobile Device
Author:      Greg Dubinovskiy 
Email:       [email protected]
Environment: VB.NET/C#
Keywords:    Web Service, File Object, DirectoryInfo
Level:       Intermediate
Description: Web Method To Display Server Directory Tree Remotely on Mobile Device
Section      Miscellaneous
SubSection   General

Introduction

One of the features available in Siccolo - Management Tool For SQL Server - ability to backup a SQL Server database from a Windows Mobile device (see my previous article at Web Method to Backup Database Remotely at Siccolo Articles). To make backup process experience friendlier and more similiar to Microsoft SQL Server Enterprise Manager, I needed to allow user select where - which directory - actual database backup file is to be saved.
So, if MS Enterprise Manager shows this:
My Siccolo Mobile Management Tool gives user this:
Idea is trivial - mobile device sends request to a web service to execute a web method. Web method uses - System.IO namespace - DirectoryInfo object and GetDirectories() method to retrieve subfolders.

(It can also be done using SQL Server - as we know anything can be done with SQL Server:

	create table #tmp (directory_name varchar(255))

	insert into #tmp
	exec master.dbo.xp_cmdshell "dir c:\ /B /A:D /S"

	select * from #tmp -- and we can produce XML out of this with "for xml auto" clause!

	drop table #tmp
see also - "How to Create a Directory (Folder) at Siccolo Articles)
    Actual implementation can be done in a few different ways:
  1. Get the complete directory structure for a given logical disk and send it to a client. Client makes synchronous web method call
  2. Get the all sub folders for a given folder and send it to a client. Client makes synchronous web method call
    (when making synchronous calls to Web services, the calling thread is blocked until the Web service returns the result of the method call.)
    - or -
  3. Get the complete directory structure for a given logical disk and send it to a client. Client makes asynchronous web method call
  4. Get the all sub folders for a given folder and send it to a client. Client makes asynchronous web method call
Because application is for mobile device, number of client-server calls needs to be limited as well as amount of data being transfered to a client from a server. Dilemma...

In the code presented, client (mobile device) pulls complete directory structure from a web service with asynchronous web method call, but displays only folder/sub folders needed at the time. (to see how to display list of disks remotely on mobilde device - Web Method To Display Server Logical Disks Remotely on Mobile Device )

1. Server Code - Retrieve Directory Structure - List of Folders

First, web method to allow client to retrieve list of logical disks on the server:

	<WebMethod()> Public Function ExplorerFoldersTree(ByVal ServerAddress As String, _
                                                            ByVal DiskPath As String, _
                                                            ByVal UseImpersonation As Boolean, _
                                                            ByVal IncludeWindowsDirectory As Boolean, _
                                                            ByRef ErrorInfo As String) As String

            Try

                Dim FoldersTree_XML As String = ""

                Dim objTree As DirectoryTree = New DirectoryTree(ServerAddress, DiskPath)


                Dim impersonationContext As System.Security.Principal.WindowsImpersonationContext

                If UseImpersonation Then
                    impersonationContext = _
                        CType(CType(User, WindowsPrincipal).Identity, _
				System.Security.Principal.WindowsIdentity).Impersonate()
                End If

                If objTree.PopulateFoldersTree_XML(FoldersTree_XML, IncludeWindowsDirectory, ErrorInfo) Then

                    FoldersTree_XML = Replace(FoldersTree_XML, "\\" & ServerAddress & "\" & Replace(DiskPath, ":", "$"), _
						DiskPath)

                    ErrorInfo = ""
                    If UseImpersonation Then
                        impersonationContext.Undo()
                    End If
                    Return FoldersTree_XML

                Else
                   
                    If UseImpersonation Then
                        impersonationContext.Undo()
                    End If

                    Return ""

                End If

            Catch ex As Exception
                ErrorInfo = ex.Message
                Return ""
            End Try

        End Function
As you can notice, I'm using impersonationContext, because web service needs to "assume" an identity of authenticated user to be able to access resources on the machine other than web service host. On the server side, to retrieve authenticated user, we need to use System.Web.Services.WebService.User and then "impersonate" - Impersonate() - to "Impersonate the user represented by the WindowsIdentity object."

List of folders is kept in m_objFoldersTree - dataset with "structured list" of folders:
	...
  <Folder>
    <f>c:\CA_LIC</f>
    <p>c:</p>
  </Folder>
  <Folder>
    <f>c:\Clients</f>
    <p>c:</p>
  </Folder>
  <Folder>
    <f>c:\Clients\Map</f>
    <p>c:\Clients</p>
  </Folder>
	...
For every "folder" element - there's a folder name (attribute <f>) and name of parent folder (attribute <p>)
m_objFoldersTree created:
	m_objFoldersTree = New DataSet("QueryResults")
        m_objFoldersTree.Tables.Add("Folder")
        m_objFoldersTree.Tables(0).Columns.Add("f", System.Type.GetType("System.String"))   'folder path
        m_objFoldersTree.Tables(0).Columns.Add("p", System.Type.GetType("System.String"))   'folder parent
then, PopulateFoldersTree_XML() creates XML String based on directory structure, from m_objFoldersTree:
   Public Function PopulateFoldersTree_XML(ByRef FoldersTree_XML As String, _
                                            ByVal IncludeWindowsDirectory As Boolean, _
                                            ByRef ErrorInfo As String) As Boolean

        Dim ServerWindowsDirectory As String = ""
        If Not IncludeWindowsDirectory Then
            'get windows directory
            ServerWindowsDirectory = Replace(GetWindowsDirectory_UsingWMI(), ":", "$")
            'exclude it from funal result
        End If

        If Not  PopulateFoldersTree(ServerWindowsDirectory, ErrorInfo) Then Return False

        Dim objStringWriter As New System.IO.StringWriter()
        m_objFoldersTree.WriteXml(objStringWriter, XmlWriteMode.WriteSchema)
        FoldersTree_XML = "" & objStringWriter.ToString()

        Return True
    End Function
In this method, PopulateFoldersTree_XML(), I'm using Dataset class to create XML string using WriteXML() method:
		...
	m_objFoldersTree = New DataSet("QueryResults")
		...	
		...
	Dim objStringWriter As New System.IO.StringWriter()
        m_objFoldersTree.WriteXml(objStringWriter, XmlWriteMode.WriteSchema)
		...
where WriteXml() Writes the current data, and the schema, for the DataSet using the specified System.IO.Stream and XmlWriteMode. XmlWriteMode.WriteSchema tells application to Write the current contents of the DataSet as XML data with the relational structure as inline XSD schema.

To add/create columns in Dataset:
	...
	m_objFoldersTree.Tables(0).Columns.Add("f", System.Type.GetType("System.String"))
	...
Columns.Add() - Creates and adds a DataColumn object to the DataColumnCollection. Here, Add() creates a DataColumn object with the specified name and type to the DataColumnCollection. To let Add() know type of column, we need to pass a parameter of type DataColumn.DataType. To do that, we can use GetType() method which gets Type with the specified name - System.Type.GetType("System.String")

In this code, it allows to exclude windows directory (such as C:\WINDOWS) and all it's subfolders from the final folders list that goes back to client - mobile device. It's done to minimize amount of data being sent to mobilde device. where GetWindowsDirectory_UsingWMI() retrieves windows directory name using WMI (can also be retrieved from registry):
    Public Function GetWindowsDirectory_UsingWMI() As String
        Try
            Dim WindowsDirectory As String = ""

            Dim query As ManagementObjectSearcher
            Dim queryCollection As ManagementObjectCollection
            Dim query_command As String = "SELECT * FROM Win32_OperatingSystem"

            Dim msc As ManagementScope = New ManagementScope("\\" & m_ServerAddress & _
                                                                "\root\cimv2")

            Dim select_query As SelectQuery = New SelectQuery(query_command)

            query = New ManagementObjectSearcher(msc, select_query)
            queryCollection = query.Get()

            Dim management_object As ManagementObject
            For Each management_object In queryCollection
                WindowsDirectory = management_object("WindowsDirectory")
            Next
            Return WindowsDirectory

        Catch ex As Exception
            Return ""
        End Try
    End Function


Now, back to "folders". For every folder, code recursively gets list of subfolders and adds them to m_objFoldersTree dataset (I used Recursive Hierarchical DataSet Techniques):
   Public Function PopulateFoldersTree(ByVal ExcludeThisDirectory As String, _
                                        ByRef ErrorInfo As String) As Boolean
        Try
            m_objFoldersTree.Tables(0).Rows.Clear()

            m_FullPathParentFolder = "\\" & m_ServerAddress & "\" & Replace(m_ParentFolder, ":", "$")
            Dim TopFolder As DirectoryInfo = New DirectoryInfo(m_FullPathParentFolder)
            Dim r As Object = New Object() {m_FullPathParentFolder, ""}
            m_objFoldersTree.Tables(0).Rows.Add(r)
            GetSubFolders(m_FullPathParentFolder, TopFolder, ExcludeThisDirectory)

            Dim objTable = m_objFoldersTree.Tables(0)
	    
            m_objFoldersTree.Relations.Add("Self", objTable.Columns("f"), _
                                            objTable.Columns("p"), False)
	    
            Return True

        Catch ex As Exception
            ErrorInfo = ex.Message
            Return False
        End Try
    End Function
    Private Sub GetSubFolders(ByVal ParentFolder As String, _
                                ByVal Folder As DirectoryInfo, _
                                ByVal ExcludeThisDirectory As String)

        Dim collFolders As DirectoryInfo() = Folder.GetDirectories()

        Dim subFolder As DirectoryInfo

        For Each subFolder In collFolders
            Try
                If subFolder.Attributes = FileAttributes.Encrypted Or _
                        subFolder.Attributes = FileAttributes.Hidden Or _
                        subFolder.Attributes = FileAttributes.Offline Or _
                        subFolder.Attributes = FileAttributes.ReadOnly Or _
                        subFolder.Attributes = FileAttributes.System Or _
                        subFolder.Attributes = FileAttributes.Temporary Or _
                        InStr(subFolder.FullName, ExcludeThisDirectory, CompareMethod.Text) > 0 Then
                    'skip this folder
                Else
                    Dim r As Object = New Object() {subFolder.FullName, ParentFolder}
                    m_objFoldersTree.Tables(0).Rows.Add(r)
                    GetSubFolders(subFolder.FullName, subFolder, ExcludeThisDirectory)
                End If
            Catch ex As Exception   'System.Security.SecurityException

            End Try
        Next

    End Sub
Because, this code is used to allow user select where database backup file is to be saved, I'm excluding server Windows directory (and all it's subfolders) and any folder that is Hidden, System etc.

And that's it for web service.

Client Code

Now the client.
User interface (form frmServerDiskExplorer, in my application) contains TreeView control trwFolders to display server directory structure:

So, when user selects a drive (C, D etc) - application calls GetServerExplorerFoldersData_PartialFillTree_Async() to:
1. get complete directory structure from web service via asynchronous web method call
2. display only top folder in TreeView

   Private Sub GetServerExplorerFoldersData_PartialFillTree_Async()
        If cboDiskList.Text = "" Then
            MessageBox.Show("Please select a drive", _
                                   "Siccolo - Server Disk Explorer", _
                                  MessageBoxButtons.OK, _
                                  MessageBoxIcon.Exclamation, _
                                  MessageBoxDefaultButton.Button1)
            Exit Sub
        End If

        Cursor.Current = Cursors.WaitCursor

        trwFolders.Nodes.Clear()

        Dim ErrorInfo As String = ""

        objSQLManager.ServerExplorerFoldersTreeForm = Me
        objSQLManager.GetServerExplorerFoldersData_Async(cboDiskList.Text, ErrorInfo)
    End Sub
Where objSQLManager - class on the client, handling all interactions with web service.
	...
	...
    Private m_objUIServerExplorerFoldersTreeForm As frmServerDiskExplorer
    Friend WriteOnly Property ServerExplorerFoldersTreeForm() As frmServerDiskExplorer
        Set(ByVal value As frmServerDiskExplorer)
            m_objUIServerExplorerFoldersTreeForm = value
        End Set
    End Property
And GetServerExplorerFoldersData_Async is the actual asynchronous method call:
   Friend Sub GetServerExplorerFoldersData_Async(ByVal DiskPath As String, _
                                                ByRef ErrorInfo As String)

        Try

            If m_objUIServerExplorerFoldersTreeForm Is Nothing Then
                Throw New Exception("User Interface Form is not set!")
            End If

            ErrorInfo = ""

            If m_objSiccoloProcessorAsync Is Nothing Then
                Dim objNetworkCredential As System.Net.NetworkCredential = _
				New System.Net.NetworkCredential(objLoginInfo.NetworkUserName, _
                                		objLoginInfo.NetworkUserPwd, _
                                                objLoginInfo.NetworkUserDomain)

                m_objSiccoloProcessorAsync = New siccolo.Siccolo()
                m_objSiccoloProcessorAsync.Credentials = objNetworkCredential
                m_objSiccoloProcessorAsync.PreAuthenticate = True
                Dim URL As String = ""
                If GetWebServiceURL(m_WebServiceSQLServerASMX, URL, ErrorInfo) Then
                    m_objSiccoloProcessorAsync.Url = URL
                Else
                    Throw New Exception(ErrorInfo)
                End If

                m_objSiccoloProcessorAsync.Timeout = System.Threading.Timeout.Infinite
            End If

            m_objSiccoloProcessorAsync.BeginExplorerFoldersTree(objLoginInfo.ServerAddress, _
                                                                    DiskPath, _
                                                                    True, _
                                                                    False, _
                                                                    ErrorInfo, _
                                                                    New AsyncCallback(AddressOf _
									Me.GetServerExplorerFoldersData_Async_CallBack), _
                                                                    Nothing)

        Catch ex As Exception
            ErrorInfo = ex.Message

            m_objUIServerExplorerFoldersTreeForm.Invoke(New AsyncCallHandler_GotServerExplorerFoldersData _
      (AddressOf m_objUIServerExplorerFoldersTreeForm.GetServerExplorerFoldersData_PartialFillTree_Async_CallBack), _
                                                    False, _
                                                    "", _
                                                    ErrorInfo)

        End Try
    End Sub
Retrieving server directory structure asynchronous operation is implemented as two methods named BeginExplorerFoldersTree and EndExplorerFoldersTree that begin and end the asynchronous operation ExplorerFoldersTree respectively.
BeginExplorerFoldersTree method takes as many parameters declared in the signature of the synchronous version of the method that are passed by value or by reference:
	'taken from Reference.vb:
		...
	'''
          _
        Public Function ExplorerFoldersTree(ByVal ServerAddress As String, _
		ByVal DiskPath As String, _
		ByVal UseImpersonation As Boolean, _
		ByVal IncludeWindowsDirectory As Boolean, 
		ByRef ErrorInfo As String) As String
            Dim results() As Object = Me.Invoke("ExplorerFoldersTree", _
		New Object() {ServerAddress,  _
				DiskPath, _
				UseImpersonation, _
				IncludeWindowsDirectory, ErrorInfo})
            ErrorInfo = CType(results(1),String)
            Return CType(results(0),String)
        End Function
        
        '''
        Public Function BeginExplorerFoldersTree(ByVal ServerAddress As String, _
						ByVal DiskPath As String, _
						ByVal UseImpersonation As Boolean, _
						ByVal IncludeWindowsDirectory As Boolean, _
						ByVal ErrorInfo As String, _
						ByVal callback As System.AsyncCallback, _
						ByVal asyncState As Object) As System.IAsyncResult
            Return Me.BeginInvoke("ExplorerFoldersTree", _
					New Object() {ServerAddress, _
							DiskPath, _
							UseImpersonation, _
							IncludeWindowsDirectory, _
							ErrorInfo}, callback, asyncState)
        End Function
        
        '''
        Public Function EndExplorerFoldersTree(ByVal asyncResult As System.IAsyncResult, _
							ByRef ErrorInfo As String) As String
            Dim results() As Object = Me.EndInvoke(asyncResult)
            ErrorInfo = CType(results(1),String)
            Return CType(results(0),String)
        End Function
		...
BeginExplorerFoldersTree method signature also includes two additional parameters - first of these defines an AsyncCallback delegate that references a method GetServerExplorerFoldersData_Async_CallBack that is called when the asynchronous operation completes:
Private Sub GetServerExplorerFoldersData_Async_CallBack(ByVal result As IAsyncResult)
        Try
            Dim ErrorInfo As String = ""
            Dim FoldersTree_XML As String = ""

            Dim CallBackResult As Boolean = True

            FoldersTree_XML = m_objSiccoloProcessorAsync.EndExplorerFoldersTree(result, _
                                                                                ErrorInfo)

            If ErrorInfo <> "" Or FoldersTree_XML = "" Then
                CallBackResult = False
            Else
                CallBackResult = True
            End If

            m_objUIServerExplorerFoldersTreeForm.Invoke(New AsyncCallHandler_GotServerExplorerFoldersData _
 (AddressOf m_objUIServerExplorerFoldersTreeForm.GetServerExplorerFoldersData_PartialFillTree_Async_CallBack), _
                                    CallBackResult, _
                                    FoldersTree_XML, _
                                    ErrorInfo)

        Catch ex_callback As Exception

            m_objUIServerExplorerFoldersTreeForm.Invoke(New AsyncCallHandler_GotServerExplorerFoldersData _
  (AddressOf m_objUIServerExplorerFoldersTreeForm.GetServerExplorerFoldersData_PartialFillTree_Async_CallBack), _
                                                   False, _
                                                   "", _
                                                   "GetServerExplorerFoldersData_Async_CallBack(): " & ex_callback.Message)
        End Try
    End Sub


The second additional parameter is a user-defined object. This object can be used to pass application-specific state information to the method invoked when the asynchronous operation completes.
BeginExplorerFoldersTree returns control to the calling thread, to frmServerDiskExplorer, immediately. If BeginExplorerFoldersTree method throws exceptions, the exceptions are thrown before the asynchronous operation is started. And if BeginExplorerFoldersTree method throws exceptions, the callback method is not invoked.
    Sequence of events:
  1. start asynchronous call with GetServerExplorerFoldersData_Async -> BeginExplorerFoldersTree()
  2. GetServerExplorerFoldersData_Async_CallBack is executed
  3. GetServerExplorerFoldersData_Async_CallBack passes control back to form via Invoke() -> m_objUIServerExplorerFoldersTreeForm.Invoke()
And the form:
   Friend Sub GetServerExplorerFoldersData_PartialFillTree_Async_CallBack _
                                            (ByVal CallBackResult As Boolean, _
                                            ByVal FoldersData_XML As String, _
                                            ByVal ErrorInfo As String)

        Try

            If Not CallBackResult Then
                Throw New Exception(ErrorInfo)
            End If

            m_ServerFoldersData = New DataSet

            Dim Reader As New System.IO.StringReader(FoldersData_XML.ToString())

            Dim XMLReader As New System.Xml.XmlTextReader(Reader)
            XMLReader.Read()

            m_ServerFoldersData.ReadXml(XMLReader, XmlReadMode.Auto)

            m_ServerFoldersData.AcceptChanges()

            XMLReader.Close()

            AddTopFolder()

        Catch ex As Exception

            MessageBox.Show("Failed to retrieve server folders data (async):" & vbCrLf & _
                            "-----------------------------------" & vbCrLf & _
                           ErrorInfo & vbCrLf & _
                           "-----------------------------------", _
                            "Siccolo - Server Disk Explorer", _
                           MessageBoxButtons.OK, _
                           MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1)

        Finally

            Cursor.Current = Cursors.Default

        End Try
    End Sub


And, when user selects a folder from tree view - application populates it with sub folders for a given folder:
    Private Sub trwFolders_AfterSelect(ByVal sender As System.Object, _
					ByVal e As System.Windows.Forms.TreeViewEventArgs) _
		Handles trwFolders.AfterSelect
        If trwFolders.SelectedNode Is Nothing Then Exit Sub

        Cursor.Current = Cursors.WaitCursor

        txtSelectedFolder.Text = trwFolders.SelectedNode.Tag
        m_SelectedPath = trwFolders.SelectedNode.Tag

       	If trwFolders.SelectedNode.Nodes.Count = 0 Then
           ShowSubFolders(trwFolders.SelectedNode)
        End If
        
        Cursor.Current = Cursors.Default
    End Sub
   Private Sub ShowSubFolders(ByRef ParentFolderNode As Windows.Forms.TreeNode)

        'find record in m_Folders where folder_parent = ParentFolder
        Dim objView As Data.DataView = _
            m_ServerFoldersData.Tables(0).DefaultView

        Dim strParentFolder As String = ParentFolderNode.Tag
        objView.RowFilter = "p='" & strParentFolder & "'"   'p is folder parent

        Dim newNode As Windows.Forms.TreeNode = New TreeNode
        Dim strNode As String
        For index As Integer = 0 To objView.Count - 1
            strNode = objView(index)("f")                   'f is folder_path
            newNode = ParentFolderNode.Nodes.Add(Replace(strNode, strParentFolder, ""))
            newNode.Tag = strNode
        Next

        If objView.Count <> 0 Then ParentFolderNode.Expand()

    End Sub
To show only sub folders for a given folder, we can use "filtering" feature with DataViews:
	...
	Dim objView As Data.DataView = _
            m_ServerFoldersData.Tables(0).DefaultView

        Dim strParentFolder As String = ParentFolderNode.Tag
        objView.RowFilter = "p='" & strParentFolder & "'"   'p is folder parent
	...
.RowFilter - Gets or sets the expression used to filter which rows are viewed in the DataView.

Points of Interest

If you would like to read more on this story - please take a look at Siccolo - Free Mobile Management Tool For SQL Server and more articles at Siccolo Articles

So, as we can see Web Services is quickly finding its way into major system integration development efforts. In back office and legacy systems alike, Web Servicess has become a main focus in providing mechanisms to make data move more freely between and across long-closed boundaries. An entire industry has joined forces and pushes forward in overcoming both technical hurdles such as security and market specific hurdles such as the need of standardized message formats. While these efforts continue, an entirely different market is rapidly maturing: the mobile devices, software and services market. Very interesting solution options arise in the cross section, where Web Services meets mobility. The Microsoft® Windows® .NET Compact Framework opens up the world of Web Services to Pocket PCs, and the wireless nature of Pocket PCs opens up the world of mobility to Web Services. This means that new fuel is added to the expansion of the XML-based Web Services provider market. In this article, by using simple Web Service Windows Mobile device is able to backup SQL Server database!


Article keywords: Backup command, Backup database, web method, web service, asynchronous method, XML, WriteXML(), ReadXML(), Dataset, impersonationContext, System.Security.Principal.WindowsImpersonationContext, WindowsPrincipal, Identity, Impersonate(), impersonationContext.Undo(), System.IO.StringWriter(), Columns.Add(), ManagementObjectSearcher, ManagementObjectCollection, ManagementScope, ManagementObject, DirectoryInfo, GetDirectories(), TreeView, Nodes(), System.Net.NetworkCredential, AsyncCallback, System.IO.StringReader(), DefaultView, DataView, System.Data, RowFilter


Back To Articles Page

Free Mobile Management For SQL Server(s!) - Siccolo - SQL Management ToolQuestions? Suggestions? Concerns? - email me to [email protected]    Greg Dubinovsky © 2006
or share your thoughts at Siccolo Blog

web sponsor - siccolo.com. well being sponsor - Enabling clinical and operational value across the continuum of care.