Following on from my chunked file uploader, I needed a way to initiate a server side import of a file and track the progress on the Silverlight clientside. To do this, I first wrote my import logic which was to be performed on the server. The important factor with the import logic, is that it raises an event whenever a line of data has been imported. I then wrote a WCF service wrapper for this, which uses a separate thread to perform the import so that the service call can return immediately after having started the import. The service comprises of 3 functions to; begin the import, check the progress, get the results. The service interface and implementation are shown below:

[ServiceContract]
    public interface IExampleImporter
    {
       
        [OperationContract]
        void BeginImportUploadedExampleFile(string uploadedFilename, bool hasHeaderRow);

        [OperationContract]
        bool CheckImportUploadedExampleFileComplete(string uploadFilename, out int rowsImported);

        [OperationContract]
        Response<ImportResult> GetImportUploadedExampleFileResults(string uploadFilename);

    }

public partial class ImportService : IExampleImporter
    {
        //create some static dictionaries for tracking the import progress and results across service calls
        private static Dictionary<string, int> _fileRowsImported = new Dictionary<string,int>();
        private static Dictionary<string, Response<ImportResult>> _fileResults = new Dictionary<string, Response<ImportResult>>();

        //we will need a class to define the params to the import thread
        private class ImportUploadedExampleFileThreadParams
        {
            public HttpContext HttpContext { get; set; }

            public string UploadFileToken { get; set; }
            public bool HasHeaderRow { get; set; }
        }
               
        //this is the service endpoint to begin a new import
        public void BeginImportUploadedExampleFile(string uploadFileToken, bool hasHeaderRow)
        {
            //you can only import a file which isnt already being imported
            if (_fileResults.ContainsKey(uploadFileToken) || _fileRowsImported.ContainsKey(uploadFileToken))
                throw new Exception("This file is already being imported");

            //initialise a place in the rows imported list
            _fileRowsImported.Add(uploadFileToken, 0);

            //we will run the import in another thread to avoid service timeouts - client can then poll 'IsUploadComplete'
            System.Threading.Thread importThread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(ImportUploadedExampleFileThreadEntry));
            importThread.IsBackground = true;

            importThread.Start(new ImportUploadedExampleFileThreadParams()
            {
                HttpContext = HttpContext.Current,
                
                UploadFileToken = uploadFileToken,
                HasHeaderRow = hasHeaderRow
                
            });

        }

        public bool CheckImportUploadedExampleFileComplete(string uploadFileToken, out int rowsImported)
        {
            //does this import exist?
            lock (_fileRowsImported)
            {
                if (!_fileRowsImported.ContainsKey(uploadFileToken))
                    throw new Exception("No current import found for the given token");

                rowsImported = _fileRowsImported[uploadFileToken];
            }

            //is it complete?
            return _fileResults.ContainsKey(uploadFileToken);
            
        }

        public Response<ImportResult> GetImportUploadedExampleFileResults(string uploadFileToken)
        {
            Response<ImportResult> res;

            //does this import exist and complete?
            lock (_fileResults)
            {
                if (!_fileResults.ContainsKey(uploadFileToken))
                    throw new Exception("Import with given token does not exist or was not yet complete");

                res = _fileResults[uploadFileToken];
                
                //now remove the result
                _fileResults.Remove(uploadFileToken);
            }

            //also remove the progress since this import is now done
            lock (_fileRowsImported)
            {
                _fileRowsImported.Remove(uploadFileToken);
            }

            return res;
        }

        private void ImportUploadedExampleFileThreadEntry(object threadParam)
        {
            //parse the thread params
            ImportUploadedExampleFileThreadParams p = (ImportUploadedExampleFileThreadParams)threadParam;

            Response<ImportResult> importResponse = new Response<ImportResult>();

            //ensure the file exists
            string fullFilePath = p.HttpContext.Server.MapPath("UploadFiles\\" + p.UploadFileToken);

            if (System.IO.File.Exists(fullFilePath))
            {
                 //create an instance of the import helper to perform the import
                 ExampleImporterHelper helper = new ExampleImporterHelper();
                 helper.RowImported += new Action<string, bool>(helper_RowImported);

                 //invoke the importer and get the result
                 importResponse.Item = helper.Import(fullFilePath, p.HasHeaderRow);
                 importResponse.IsSuccess = true;

            }
            else
            {
                importResponse.IsSuccess = false;
                importResponse.MessageKey = "File not found";
            }

            lock (_fileResults)
            {
                _fileResults.Add(p.UploadFileToken, importResponse);
            }
        }

        void helper_RowImported(string filename, bool successfulRow)
        {
            //parse the token from the filename
            string token = System.IO.Path.GetFileName(filename);

            //update the rows imported
            lock (_fileRowsImported)
            {
                if (_fileRowsImported.ContainsKey(token))
                    _fileRowsImported[token] += 1;
            }

        }

    }
The way this works is, after having uploaded a file to the server, the client calls 'BeginImport..' passing the filename/token which initiates the import. The service creates a new thread which runs the actual import code passing in the required parameters. The service maintains two Dictionaries, one which stores the current number of lines imported by filename (by counting the RowImported events) and one which stores the eventual 'ImportResult' - which exists once the import is complete. Calling the 'is complete' function will return true or false and additionally will return the number of lines imported through the 'out' parameter. This enables the client to determine if an upload has finished and if not, how many lines are complete so far - which can be reflected in the UI based on the total number of lines. My particular example relies on the following data contracts to transmit the results:

[DataContract]
    public class ImportResult
    {
        [DataMember]
        public int SuccessCount { get; set; }

        [DataMember]
        public int FailCount { get; set; }

        private List<ImportException> _Exceptions = new List<ImportException>();

        [DataMember]
        public List<ImportException> Exceptions
        {
            get { return _Exceptions; }
            set { _Exceptions = value; }
        }


    }
    [DataContract]
    public class ImportException
    {
        [DataMember]
        public int LineNumber { get; set; }

        public Exception Exception { get; set; }

        [DataMember]
        public string ErrorMessage
        {
            get
            {
                return Exception.Message;
            }
            private set { }
        }

        public ImportException(int lineNumber, Exception ex)
        {
            this.LineNumber = lineNumber;
            this.Exception = ex;
        }
    }
An example of calling the service in Silverlight (using MVVM viewmodel) is as follows:

public void ImportUploadedFile(string serverToken)
        {

            ImportProgressText = string.Format("[{0}] Starting Import. (this may take a while)\r\n", DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));

            //in part 4 we actually import the file and get the results of the import
            IsImporting = true;

            //Start the Example importer
            ServiceInerfaceClient svc = ServiceUtility.GetExampleClient();
            svc.BeginImportUploadedExampleFileCompleted += new EventHandler<AsyncCompletedEventArgs>(svc_BeginImportUploadedExampleFileCompleted);
            svc.BeginImportUploadedExampleFileAsync(serverToken, HasHeaderRow);
        }

        void svc_BeginImportUploadedExampleFileCompleted(object sender, AsyncCompletedEventArgs e)
        {
            ((ServiceInerfaceClient)sender).BeginImportUploadedExampleFileCompleted -= svc_BeginImportUploadedExampleFileCompleted;

            //check no errors launching import
            if (e.Error == null)
            {
                //now that the import has successfully started we can begin polling for the results
                PollForImportResults();
            }
            else
            {
                IsImporting = false;
                ShowBusy(false);

                if (e.Error != null)
                    ImportProgressText += string.Format("[{0}] ERROR: " + e.Error.Message, System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                else
                    ImportProgressText += string.Format("[{0}] ERROR: Unknown Error Importing File", System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
            }
        }

        private void PollForImportResults()
        {
            //set up a client and the event handler
            ServiceInerfaceClient svc = ServiceUtility.GetExampleClient();
            svc.CheckImportUploadedExampleFileCompleteCompleted += new EventHandler<CheckImportUploadedExampleFileCompleteCompletedEventArgs>(svc_CheckImportUploadedExampleFileCompleteCompleted);

            //start the recursion on another thread (prevent freezing the UI)
            System.Threading.Thread pollThread = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(PollForImportResults_Recursion));
            pollThread.IsBackground = true;
            pollThread.Start(svc);

        }

        private void PollForImportResults_Recursion(object ServiceInerfaceClientInstance)
        {
            //wait 5 secs
            System.Threading.Thread.Sleep(5000);

            //call the service and wait for the poll result
            ServiceInerfaceClient svc = (ServiceInerfaceClient)ServiceInerfaceClientInstance;
            svc.CheckImportUploadedExampleFileCompleteAsync(SelectedFile.ServerToken);
        }

        void svc_CheckImportUploadedExampleFileCompleteCompleted(object sender, CheckImportUploadedExampleFileCompleteCompletedEventArgs e)
        {
            //make sure no errors reading polling service
            if (e.Error == null)
            {
                //update the rows imported counter
                //update the UI via the dispatcher thread
                Deployment.Current.Dispatcher.BeginInvoke(new Action<ExampleAdminViewModel>((vm) =>
                    {
                        vm.RowsProcessed = e.rowsImported;
                    }), this);

                //was the import complete?
                if (e.Result == true)
                {
                    //my job now done, remove the handler
                    ((ServiceInerfaceClient)sender).CheckImportUploadedExampleFileCompleteCompleted -= svc_CheckImportUploadedExampleFileCompleteCompleted;

                    //update the UI via the dispatcher thread
                    Deployment.Current.Dispatcher.BeginInvoke(new Action<ExampleAdminViewModel>((vm) =>
                        {
                            vm.ImportProgressText += string.Format("[{0}] Import Complete, retreiving results.\r\n", System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                        }), this);

                    //finish the import to get the results
                    GetImportResults();
                }
                else
                {
                    //still not finished, continue with the recursion
                    PollForImportResults_Recursion((ServiceInerfaceClient)sender);
                }
            }
            else
            {
                //display errors using dispatcher (UI) thread
                Deployment.Current.Dispatcher.BeginInvoke(new Action<ExampleAdminViewModel, CheckImportUploadedExampleFileCompleteCompletedEventArgs>((vm, UIe) =>
                {
                    vm.IsImporting = false;
                    vm.ShowBusy(false);

                    if (UIe.Error != null)
                        vm.ImportProgressText += string.Format("[{0}] ERROR: " + UIe.Error.Message, System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                    else
                        vm.ImportProgressText += string.Format("[{0}] ERROR: Unknown Error Importing File", System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                }), this, e);
               
            }
        }

        private void GetImportResults()
        {
            ServiceInerfaceClient svc = ServiceUtility.GetExampleClient();

            svc.GetImportUploadedExampleFileResultsCompleted += new EventHandler<GetImportUploadedExampleFileResultsCompletedEventArgs>(svc_GetImportUploadedExampleFileResultsCompleted);
            svc.GetImportUploadedExampleFileResultsAsync(SelectedFile.ServerToken);
        }

        void svc_GetImportUploadedExampleFileResultsCompleted(object sender, GetImportUploadedExampleFileResultsCompletedEventArgs e)
        {
            
            if (e.Error == null && e.Result != null && e.Result.IsSuccess)
            {
                ImportResult res = e.Result.Item;

                //file has finished importing, calculate a string to represent the results
                string importResultsText = string.Format(@"[{0}] Results:

----------------------------------------
Successful rows: {1}
Failed rows: {2}

Failure breakdown:
---------------------------------------

",
                                System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"),
                                res.SuccessCount,
                                res.FailCount);

                foreach (ImportException iEx in res.Exceptions)
                {
                    importResultsText += string.Format("Line: {0} - {1}\r\n", iEx.LineNumber, iEx.ErrorMessage);
                }

                //display results using dispatcher (UI) thread
                Deployment.Current.Dispatcher.BeginInvoke(new Action<ExampleAdminViewModel>((vm) =>
                {
                    vm.ImportProgressText += importResultsText;
                    vm.IsImporting = false;
                    vm.ShowBusy(false);
                }), this);
            }
            else
            {

                //display errors using dispatcher (UI) thread
                Deployment.Current.Dispatcher.BeginInvoke(new Action<ExampleAdminViewModel, GetImportUploadedExampleFileResultsCompletedEventArgs>((vm, UIe) =>
                {
                    vm.IsImporting = false;
                    vm.ShowBusy(false);

                    if (UIe.Result != null)
                        vm.ImportProgressText += string.Format("[{0}] ERROR: " + UIe.Result.MessageKey, System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                    else if (UIe.Error != null)
                        vm.ImportProgressText += string.Format("[{0}] ERROR: " + UIe.Error.Message, System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                    else
                        vm.ImportProgressText += string.Format("[{0}] ERROR: Unknown Error Importing File", System.DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss"));
                }), this, e);

               
            }

        }
This is again using threads to perform the polling of the server so that calls to Thread.Sleep can be made on the polling thread without freezing the UI. I have used 'BeginInvoke' on the 'Dispatcher' thread when setting ViewModel properties, as this indirectly updates the UI (through the bindings) and so must take place on the main UI thread.