You are here:   Blog
Register   |  Login

LightSwitch News

 

Jan 17

Written by: Michael Washington
1/17/2012 6:41 PM  RssIcon

Online Demo: http://lightswitchhelpwebsite.com/Demos/ExpenseReport.aspx

Download project: http://lightswitchhelpwebsite.com/Downloads.aspx

Note: The author has received compensation from ComponentOne for the following article:

In this article we will  cover a project that implements the ComponentOne Excel control, from the Studio for Silverlight suite, in Visual Studio LightSwitch. Specifically, it will demonstrate importing and exporting Microsoft Excel files. The sample project expands on the Expense Report application covered in the article: Using the ComponentOne FlexGrid Control in Visual Studio LightSwitch.

This project enables the following scenarios:

  • Allow employees to use an Excel spread sheet to create their expense reports.
  • Display the Excel spread sheet in the ComponentOne FlexGrid control, with all formatting and formulas, and allow it to be edited before importing.
  • Import the Excel spread sheet into the Expense Report application.
  • Export existing expense reports to the control and save them as Excel files.

Walk-Thru

image

When we open the application, we can create a new report, or open an existing one, by selecting it on the Dashboard screen.

image

We can click the Export To Excel button to open the report on the Excel screen.

image

When the report opens on the Excel screen in the ExcelBook control (that implements the ComponentOne FlexGrid control), we notice that it implements Excel formatting and formulas.

image

We click the Save button on the control to save the report.

image

This allows us to save the report as a normal Excel file.

image

We can open the file in Microsoft Excel and add, edit, and remove data.

image

We can return to the LightSwitch application and open the Excel file.

image

Any changes to the Excel file will show, and we can make further edits in the control if needed.

image

When we click Import, custom code determines if this is an existing report to update or a new report to insert (based on the report name, report date, and the user importing the report).

image

When the data is imported, we see that all normal LightSwitch business rules are processed.

Set-Up

image

To set-up the project, we install the ComponentOne Studio for Silverlight suite. It will automatically create shortcuts in our Windows Start Menu. We select Samples under Studio for Silverlight.

image

When the ComponentOne Sample Explorer opens, we select the ExcelBook sample and launch it.

image

When we launch the ExcelBook solution and load a sample Excel report, we see that it implements the ComponentOne FlexGrid for Silverlight control to display the Excel files. The ExcelBook sample contains a number of features such as filtering, sorting, grouping, copy and paste, redo and undo, and printing.

One of the most fascinating things about this sample is how it implements the Calculation Engine that allows the FlexGrid control to implement Excel formulas. The Calculation Engine is covered in detail here: http://our.componentone.com/2011/08/19/a-calculation-engine-for-flexgrid-part-2-silverlight/.

image

In LightSwitch, we create a Screen and add a Custom Control.

image

We reference the ExcelBook and FlexGridBook projects that make up the ExcelBook sample solution.

image

We also add references to the C1.Silverlight.FlexGrid.4 and C1.Silverlight.FlexGridFilter.4 assemblies because we will implement code later inside LightSwitch that will require them.

We select MainPage from the ExcelBook project as the control to insert on the Screen.

image

As covered in the article: Databinding A Complex Multi-Table Silverlight Control In LightSwitch: ComponentOne Scheduler, we create a public property in the code behind of the Silverlight control to enable programmatic access of the control in LightSwitch.

image

When we run the application and select the Excel screen, the control displays.

Creating a New Excel File in the Control

image

In the sample download, we have implemented a New Report button on the Excel screen.

image

This creates a blank expense report that, for example, could be given to employees to complete when they are on the road and may not have access to the LightSwitch application.

To create a valid expense report (that can be imported later), we will use the following rules:

  • Cell A1 must contain Report Name: followed by a report name.
  • Cell A2 must contain Report Date: followed by a valid date for the report.
  • Starting at cell B5, the column must contain a valid expense date
  • Starting at cell C5, the column must contain a valid expense description.
  • Starting at cell D5, the column must contain a valid expense amount.
  • A cell in column C must contain the word Total (so the import knows when to stop importing expenses).

The following code is used to implement the New Report button:

 

        // Create an Excel Book with one Sheet
        var book = new C1XLBook();
        C1.Silverlight.Excel.XLSheet sheet = book.Sheets[0];
        // set column widths
        sheet.Columns[0].Width = 3000;
        sheet.Columns[1].Width = 1000;
        sheet.Columns[2].Width = 2000;
        sheet.Columns[3].Width = 1000;
        // Set report name
        sheet[0, 0].Value = string.Format("Report Name: {0}",
            "My Report");
        // set report date
        sheet[1, 0].Value = string.Format("Report Date: {0}", 
            DateTime.Now.ToShortDateString());
        // make headers for expense report columns
        sheet[3, 1].Value = "Date";
        sheet[3, 2].Value = "Description";
        sheet[3, 3].Value = "Amount";
        // set total cell
        sheet[12, 2].Value = "Total";
        // set formula for total cell
        sheet[12, 3].Formula = "=SUM(D3:D12)";
        // **** Formatting  
        // create currency cells format
        var styleCurrency = new XLStyle(book);
        styleCurrency.Format = "$ .00";
        // Format a few rows with currency format
        for (int i = 4; i < 13; i++)
        {
            XLCell CurrencyCell = sheet[i, 3];
            CurrencyCell.Style = styleCurrency;
        }
        // create expense header cells format
        var styleText = new XLStyle(book);
        styleText.AlignHorz = XLAlignHorzEnum.Center;
        styleText.BackColor = Colors.LightGray;
        // Format the header cells
        sheet[3, 1].Style = styleText;
        sheet[3, 2].Style = styleText;
        sheet[3, 3].Style = styleText;
        // create total cell format
        var styleTotal = new XLStyle(book);
        styleTotal.AlignHorz = XLAlignHorzEnum.Right;
        styleTotal.BackColor = Colors.LightGray;
        styleTotal.Font = new XLFont("Arial", 12, true, false);
        // format the total cell
        sheet[12, 2].Style = styleTotal;
        Dispatchers.Main.BeginInvoke(() =>
        {
            // Create a memory stream
            using (MemoryStream memory = new MemoryStream())
            {
                // Save the book to the stream
                book.Save(memory);
                // load the book from the stream
                _flex.Load(memory);
            }
        });

 

Note: Access to the Excel formatting features is available only through the C1XLBook class. We must create the Excel spread sheet using that class and then load it into the C1FlexGridBook object that displays the spread sheet.

Importing an Excel File

image

When we open an existing Excel file and click the Import button, we need to determine if it is an existing report or a new one. If it is an existing report we need to update the report.

The first thing we do is gather the required values from the control:

 

            // Get the required values
            string strReportName = "";
            DateTime dtReportDate = new DateTime();
            int intTotalRow = 0;
            // Get Report Name
            string strRawReportName = Convert.ToString(_flex[0, 0]);
            strReportName = strRawReportName.Replace("Report Name: ", "");
            // Get Report Date
            string strRawReportDate = Convert.ToString(_flex[1, 0]);
            strRawReportDate = strRawReportDate.Replace("Report Date: ", "");
            DateTime.TryParse(strRawReportDate, out dtReportDate);
            // Get Total Row
            // Loop through all rows starting at row 4, looking for 'Total'
            for (int i = 4; i < _flex.Rows.Count; i++)
            {
                if (Convert.ToString(_flex[i, 2]) == "Total")
                {
                    intTotalRow = i;
                    break;
                }
            }

 

Next, we validate that we have all required fields:

 

            // Validate that all fields are found
            if (strReportName == ""
                || dtReportDate < new DateTime(1900, 1, 1)
                || intTotalRow == 0)
            {
                Dispatchers.Main.BeginInvoke(() =>
                {
                    // show error that we cannot import
                    MessageBox.Show("Cannot Import");
                });
                return;
            }

 

We attempt to locate the report, and if we cannot find it, we create a new one; otherwise, we open the existing report and delete all the existing expense items:

 

            // Locate an expense report that matches the current one
            this.ExpenseReport = (from ExpenseReport in 
                                      this.DataWorkspace.ApplicationData.ExpenseReports
                                      .GetQuery().Execute()
                                  where ExpenseReport.UserName == this.Application.User.Name
                                  where ExpenseReport.ReportName == strReportName
                                  where ExpenseReport.ReportDate == dtReportDate
                                  select ExpenseReport).FirstOrDefault();
            if (this.ExpenseReport == null)
            {
                // Create a new Expense Report
                this.ExpenseReport = new ExpenseReport();
                this.ExpenseReport.ReportDate = dtReportDate;
                this.ExpenseReport.ReportName = strReportName;
            }
            else
            {
                // Update Expense Report
                // First, Delete all existing Expense Report Details
                var colReportDetails = from ReportDetails in this.ExpenseReport.ReportDetails
                                       select ReportDetails;
                foreach (var item in colReportDetails)
                {
                    item.Delete();
                }
            }

 

Now, we import the expense line items:

 

            // Import the Report Details
            // Get the Expense Details from the Excel control
            for (int i = 4; i < intTotalRow; i++)
            {
                DateTime dtExpenseDetailDate = new DateTime();
                decimal dExpenseDetailAmount = 0.00M;
                // Gather Raw values
                var strRawDate = Convert.ToString(_flex[i, 1]);
                var strRawAmount = Convert.ToString(_flex[i, 3]);
                // Cast the values
                DateTime.TryParse(strRawDate, out dtExpenseDetailDate);
                Decimal.TryParse(strRawAmount, out dExpenseDetailAmount);
                // Get the description
                var strExpenseDetailDescription = Convert.ToString(_flex[i, 2]);
                // Validate that all fields are found
                if (strExpenseDetailDescription != ""
                    && dtExpenseDetailDate > new DateTime(1900, 1, 1)
                    && dExpenseDetailAmount > 0)
                {
                    // Add Expense Detail 
                    var objReportDetails = this.ExpenseReport.ReportDetails.AddNew();
                    objReportDetails.ExpenseDate = dtExpenseDetailDate;
                    objReportDetails.ExpenseDescription = strExpenseDetailDescription;
                    objReportDetails.ExpenseRequestedAmount = dExpenseDetailAmount;
                }
            }
            // Save the Expense Report
            this.Save();

 

Finally, we open the report in the Report Details screen (or switch to it if it is already open), and refresh the data (in case the screen is already open):

 

            // Open the Expense Report in the Details Screen
            Application.ShowExpenseReportDetail(this.ExpenseReport.Id);
            // Get a reference to the ExpenseReport Details screen 
            Microsoft.LightSwitch.Client.IActiveScreen 
                ExpenseReportDetailScreen = Application.ActiveScreens
                .Where(a => a.Screen is ExpenseReportDetail).FirstOrDefault();
            if (ExpenseReportDetailScreen != null)
            {
                ExpenseReportDetailScreen.Screen.Details.Dispatcher
                    .BeginInvoke(() =>
                {
                    // Refresh the ExpenseReportDetail Screen 
                    ((ExpenseReportDetail)ExpenseReportDetailScreen.Screen)
                        .Refresh();
                });
            }

 

Exporting an Existing Report to the ExcelBook Control

image

The final feature we will implement will allow us to open an existing expense report record and export it to the ExcelBook control.

image

We add an Integer property to the Excel screen called ExpenseReportID. We ensure that Is Required is not checked because we still want to allow the option to open the screen without passing an ExpenseReportID.

image

We then select Is Parameter in the properties of the ExpenseReportID property.

This will cause LightSwitch to automatically create a ShowExcel(Nullable<int> ExpenseReportID) method that will allow another screen to open the Excel screen and optionally pass an ExpenseID.

image

We also add an ExpenseReport entity and an associated ReportDetails collection to the screen.

Note: It is important that we add this entity and collection to the screen so that the LightSwitch code that we will create will have entities and collections to use when loading the data. While it is possible to load the data using entities and collections created entirely in code, we will run into challenges related to the dual-threading that LightSwitch uses to process user interface code and background processes. Adding the entities and collections to the screen and hydrating them provides less friction when implementing our custom code.

In the InitializeDataWorkspace method of the Excel screen, we implement code that will load the expense report if passed an ExpenseReportID:

 

        partial void Excel_InitializeDataWorkspace(List<IDataService> saveChangesTo)
        {
            // If an ExpenseReportID was passed...
            if (this.ExpenseReportID != null)
            {
                // Load the Expense Report into the ExpenseReport property
                this.ExpenseReport =
                    this.DataWorkspace.ApplicationData.
                    ExpenseReports_SingleOrDefault(this.ExpenseReportID);
                // Refresh the ReportDetails query
                // to pull in all the data now
                this.ReportDetails.Refresh();
                // Set the name of the Tab
                this.SetDisplayNameFromEntity(this.ExpenseReport);
            }
        }

 

Notice the ReportDetails.Refresh() line. I have found that without implementing this in the InitializeDataWorkspace method, the report details would not load, even when you implement code in other methods to explicitly load the data.

The operation to open the Excel screen and load a spread sheet requires us to interact with the ExcelBook control without the normal process of having a screen fully load and waiting for an end-user to click a button.

We must wire-up a method to notify us when the screen is loaded and the ExcelBook (that implements the FlexGrid control) is available:

 

    partial void Excel_Created()
    {
        // Get an instance of the FlexGrid Control
        c1FlexGridProxy = this.FindControl("SilverlightControl");
        // Create a handler to fire when the control is actually available            
        c1FlexGridProxy.ControlAvailable +=
            new EventHandler<ControlAvailableEventArgs>
                (c1FlexGridProxy_ControlAvailable);
    }

 

When the method is called, it allows us to then perform the import:

 

    void c1FlexGridProxy_ControlAvailable
        (object sender, ControlAvailableEventArgs e)
    {
        // Get an instance of the Silverlight Control
        System.Windows.Controls.UserControl objUserControl =
            e.Control as System.Windows.Controls.UserControl;
        // Get an instance of the FlexGrid Control
        _flex = objUserControl.FindName("_flex") as C1FlexGridBook;
        // Remove handler           
        c1FlexGridProxy.ControlAvailable -=
            new EventHandler<ControlAvailableEventArgs>
                (c1FlexGridProxy_ControlAvailable);
        // If an ExpenseReportID was passed...
        if (this.ExpenseReportID != null)
        {
            // Load the Expense Report
            LoadExpenseReport();
        }
    }

 

The LoadExpenseReport method resembles the New Report code with this additional code to gather the expense details:

 

        // load the expense details
        int intHighestRow = 4;
        foreach (var ExpenseDetail in ReportDetails)
        {
            var ExpenseDate = ExpenseDetail.ExpenseDate.ToShortDateString();
            var ExpenseDescription = ExpenseDetail.ExpenseDescription;
            var ExpenseAmount = ExpenseDetail.ExpenseRequestedAmount;
            sheet[intHighestRow, 1].Value = ExpenseDate;
            sheet[intHighestRow, 2].Value = ExpenseDescription;
            sheet[intHighestRow, 3].Value = ExpenseAmount;
            intHighestRow++;
        }

 

The code required for the Export To Excel button on the Excel screen is very simple:

 

        partial void ExportToExcel_Execute()
        {
            // Save the Expense Report
            this.Save();
            // Open the Expense Report in the Excel Screen
            Application.ShowExcel(this.ExpenseReport.Id);
        } 

 

Big Functionality - Small Price

The ComponentOne ExcelBook is a feature-rich control that still requires a considerable amount of back-end features to be implemented to process the Excel files that it loads. Using it with LightSwitch allows us to implement these requirements in a fraction of the time because most of the required features, such as screen management and saving and loading records from the database, are already built-into LightSwitch.

 

The Code

Online Demo: http://lightswitchhelpwebsite.com/Demos/ExpenseReport.aspx

Download project: http://lightswitchhelpwebsite.com/Downloads.aspx

Resources

Online help for ComponentOne Excel: http://helpcentral.componentone.com/nethelp/c1excelSL/

Notes

  • When you download the code, you may need to right-click on the .zip file and select Unblock, so you can fully build the project.
  • When performing an import of an Excel file, close any report detail tabs first for it to properly open the tab after the import (the import will always import correctly). Due to the async calls, a surprisingly large amount of code was required in a number of places to properly open the correct tab, so I left it out because it made it hard to understand the important code that is the point of the article.

8 comment(s) so far...


Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

Is this desktop only?

Either way it reaches new levels of awesomeness!

By The_Regan on   1/17/2012 9:51 PM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

@The_Regan - This works in a Web LightSwitch application or a Desktop application.

By Michael Washington on   1/18/2012 5:10 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

Michael,

This is GREAT!! There are so many opportunities with LightSwitch, and it's contributions like this that make it easy to answer those, "...well can it do this...?" questions.

By the way, I love the use of the FlexGrid with grouping and hyperlinks.

Cheers!

Paul

By Paul on   1/18/2012 6:46 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

@Paul - Thank you for reading and the feedback, it is appreciated.

By Michael Washington on   1/18/2012 9:30 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

This may be your best article yet Michael - another phenomenal way to show how powerful Lightswitch is, especially when combined with the tools from a company as great as ComponentOne!

By Chris on   1/18/2012 9:42 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

@Chris - I agree, this article feels like the best I have done so far.

By Michael Washington on   1/18/2012 9:50 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

Bravo Mickael ! Very usefull indeed. exactly what I was looking for !
If I may ask for more, is there a VB version of the code ?

By Georges BESSIS on   1/11/2013 8:14 AM
Gravatar

Re: Using the ComponentOne Excel for Silverlight Control with LightSwitch

@Georges BESSIS - Sorry no.

By Michael Washington on   1/11/2013 8:48 AM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation