Using LightSwitch For The Base Of All Your Projects

The number one reason you would not use LightSwitch, is that you have a web application that needs to accessed by clients who cannot, or will not, install the Silverlight plug-in.

For example, if we create the application covered here: http://www.codeproject.com//KB/silverlight/OnlineOrdering.aspx and we decide we want to allow Customers to place orders with their IPad, we would assume that we cannot use LightSwitch for the project. This article will demonstrate, that it is easy to add normal HTML pages, to allow access to the LightSwitch application.

Because LightSwitch uses a normal SQL database, this is not difficult. However, one of the benefits of Lightswitch, is that it allows you to easily create and maintain business rules. These business rules are the heart (and the point) of any application. You want any 'web-based client' to work with any LightSwitch business rules. In addition, you want the security to also work.

For example, In the LightSwitch Online Ordering project, we allow an administrator to place orders for any Customer, but when a Customer logs in, they can only see, and place orders for themselves. This article will demonstrate how this can be implemented in HTML pages. If a change is made to the LightSwitch application, the HTML pages automatically get the new business logic without needing to be re-coded.

The Sample Application

We first start with the application covered here: http://www.codeproject.com//KB/silverlight/OnlineOrdering.aspx.

We add some pages to the deployed application.

We log into the application using normal Forms Authentication.

These pages work fine with any standard web browser including an IPad.

A Customer is only able to see their orders.

The Customer can place an order.

All the business rules created in the normal LightSwitch application are enforced.

How It's Done

The key to making this work is that LightSwitch is built on top of standard Microsoft technologies such as RIA Domain Services. While this is not normally done, it is possible to connect to RIA Domain Services using ASP.NET code.

One blog covers doing this using MVC: http://blog.bmdiaz.com/archive/2010/04/03/using-ria-domainservices-with-asp.net-and-mvc-2.aspx. As a side note, you can use MVC to create web pages. I originally created this sample using MVC but switched back to web forms because that is what I code in mostly.

The Service Provider

It all starts with the Service Provider. When calling RIA Domain Services from ASP.NET code, you need a Service Provider. Min-Hong Tang of Microsoft provided the basic code that I used to create the following class:

public class ServiceProvider : IServiceProvider
{
    public ServiceProvider(HttpContext htpc)
    {
        this._httpContext = htpc;
    }
    private HttpContext _httpContext;
    public object GetService(Type serviceType)
    {
        if (serviceType == null)
        {
            throw new ArgumentNullException("serviceType");
        }
        if (serviceType == typeof(IPrincipal))
            return this._httpContext.User;
        if (serviceType == typeof(HttpContext))
            return this._httpContext;
        return null;
    }
}

The Domain Service

Next we have the Domain Service. This is the heart of the application. The LightSwitch team intentionally does not expose any of the WCF service points that you would normally connect to. The LightSwitch Domain Service is one of the few LightSwitch internal components you can get to outside of the normal LightSwitch client.

The following class uses the Service Provider and returns a Domain Service that will be used by the remaining code.

using System.Web;
using System.ServiceModel.DomainServices.Server;
using LightSwitchApplication.Implementation;

public class LightSwitchDomainService
{
    public static ApplicationDataDomainService 
        GetLightSwitchDomainService(HttpContext objHtpContext, DomainOperationType DOT)
    {
        // Create a Service Provider
        ServiceProvider sp = new ServiceProvider(objHtpContext);

        // Create a Domain Context 
        // Set DomainOperationType (Invoke, Metdata, Query, Submit)
        DomainServiceContext csc = new DomainServiceContext(sp, DOT);

        // Use the DomainServiceContext to instantiate an instance of the 
        // LightSwitch ApplicationDataDomainService
        ApplicationDataDomainService ds =
            DomainService.Factory.CreateDomainService(typeof(ApplicationDataDomainService), csc) 
            as ApplicationDataDomainService;

        return ds;
    }
}

Authentication

When we created the original application, we selected Forms Authentication. LightSwitch simply uses standard ASP.NET Forms Authentication. This allows you to insert a LightSwitch application into an existing web site and use your existing users and roles.

I even inserted a LightSwitch application into a DotNetNuke website (I will cover this in a future article).

We can simply drag and drop a standard ASP.NET Login control and drop it on the page.

I only needed to write a small bit of code to show and hide panels when the user is logged in:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (this.User.Identity.IsAuthenticated) // User is logged in
        {
            // Only run this if the panel is hidden
            if (pnlLoggedIn.Visible == false)
            {
                pnlNotLoggedIn.Visible = false;
                pnlLoggedIn.Visible = true;

                ShowCustomerOrders();
            }
        }
        else // User not logged in
        {
            pnlNotLoggedIn.Visible = true;
            pnlLoggedIn.Visible = false;
        }
    }

You can even open the site in Visual Studio, and go into the normal ASP.NET Configuration...

And manage the users and roles.

A Simple Query

When a user logs in, a simple query shows them the Orders that belong to them:

    private void ShowCustomerOrders()
    {
        // Make a collection to hold the final results
        IQueryable<Order> colOrders = null;

        //// Get LightSwitch DomainService
        ApplicationDataDomainService ds =
            LightSwitchDomainService.GetLightSwitchDomainService(this.Context, DomainOperationType.Query);

        // You can't invoke the LightSwitch domain Context on the current thread
        // so we use the LightSwitch Dispatcher
        ApplicationProvider.Current.Details.Dispatcher.BeginInvoke(() =>
        {
            // Define query method
            DomainOperationEntry entry = serviceDescription.GetQueryMethod("Orders_All");

            // execute query
            colOrders = (IQueryable<Order>)
                ds.Query(new QueryDescription(entry, new object[] { null }),
                out errors, out totalCount).AsQueryable();
        });

        // We now have the results - bind then to a DataGrid
        GVOrders.DataSource = colOrders;
        GVOrders.DataBind();
    }

When a user clicks on the Grid, this code is invoked:

    protected void GVOrders_RowCommand(object sender, GridViewCommandEventArgs e)
    {
        if (e.CommandName == "Select")
        {
            LinkButton lnkButtton = (LinkButton)e.CommandSource;
            int intOrderID = Convert.ToInt32(lnkButtton.CommandArgument);

            ShowOrderDetails(intOrderID);
        }
    }

This code invokes a slightly more complex series of queries because we must display the Product names, show the total for all Order items, and format the output using a custom class.

    private void ShowOrderDetails(int intOrderID)
    {
        // Make a collection to hold the final results
        List<OrderDetailsInfo> colOrderDetailsInfo = new List<OrderDetailsInfo>();

        // A value to hold order total
        decimal dOrderTotal = 0.00M;

        // Make collections to hold the results from the Queries
        IQueryable<Product> colProducts = null;
        IQueryable<OrderDetail> colOrderDetails = null;

        // Get LightSwitch DomainService
        ApplicationDataDomainService ds =
            LightSwitchDomainService.GetLightSwitchDomainService(this.Context, DomainOperationType.Query);

        // You can't invoke the LightSwitch domain Context on the current thread
        // so we use the LightSwitch Dispatcher
        ApplicationProvider.Current.Details.Dispatcher.BeginInvoke(() =>
        {
            // Define query method
            DomainOperationEntry entry = serviceDescription.GetQueryMethod("Products_All");

            // execute query
            colProducts = (IQueryable<Product>)
                ds.Query(new QueryDescription(entry, new object[] { null }),
                out errors, out totalCount).AsQueryable();
        });

        ApplicationProvider.Current.Details.Dispatcher.BeginInvoke(() =>
        {
            // Define query method
            DomainOperationEntry entry = serviceDescription.GetQueryMethod("OrderDetails_All");

            // execute query
            colOrderDetails = (IQueryable<OrderDetail>)
                ds.Query(new QueryDescription(entry, new object[] { null }),
                out errors, out totalCount).AsQueryable();
        });

        // We only want the Order details for the current order
        var lstOrderDetails = colOrderDetails.Where(x => x.OrderDetail_Order == intOrderID);

        foreach (var Order in lstOrderDetails)
        {
            // Get the Product from the ProductID
            var objProduct =
                colProducts.Where(x => x.Id == Order.OrderDetail_Product).FirstOrDefault();

            // Add the item to the final collection
            colOrderDetailsInfo.Add(new OrderDetailsInfo
            {
                Quantity = Order.Quantity,
                ProductName = objProduct.ProductName,
                ProductPrice = objProduct.ProductPrice,
                ProductWeight = objProduct.ProductWeight
            });

            // Update order total
            dOrderTotal = dOrderTotal + (objProduct.ProductPrice * Order.Quantity);
        }

        // We now have the results - bind then to a DataGrid
        gvOrderDetails.DataSource = colOrderDetailsInfo;
        gvOrderDetails.DataBind();

        lblOrderTotal.Text = String.Format("Order Total: ${0}", dOrderTotal.ToString());
    }

Inserting Data

You can click the New Order link to switch to the New Order form and create a new Order.

If we look at the table structure in SQL Server, we see that when you click Submit, an Order entity needs to be created, and any OrderDetail entities need to be added to it.

In addition, we need to capture any errors and display them.

The following code does that:

    protected void btnSubmitOrder_Click(object sender, EventArgs e)
    {
        try
        {
            // Hold the results from the Queries
            IQueryable<Product> colProducts = null;

            // Get LightSwitch DomainService
            ApplicationDataDomainService ds =
                LightSwitchDomainService.GetLightSwitchDomainService(this.Context, DomainOperationType.Submit);

            // Get the ID of the Customer Selected        
            int CustomerID = Convert.ToInt32(ddlCustomer.SelectedValue);

            // We will need all the Products  
            // lets reduce hits to the database and load them all now 
            ApplicationProvider.Current.Details.Dispatcher.BeginInvoke(() =>
            {
                colProducts = ds.Products_All("");
            });

            // Create New Order
            Order NewOrder = Order.CreateOrder
                (
                    0,
                    Convert.ToDateTime(txtOrderDate.Text),
                    ddlOrderStatus.SelectedValue,
                    CustomerID
                );

            // Get the OrderDetails
            if (txtQuantity1.Text != "")
            {
                int ProductID1 = Convert.ToInt32(ddlProduct1.SelectedValue);

                OrderDetail NewOrderDetail1 =
                    new OrderDetail();
                NewOrderDetail1.Quantity =
                    Convert.ToInt32(txtQuantity1.Text);
                NewOrderDetail1.Product =
                    colProducts.Where(x => x.Id == ProductID1)
                        .FirstOrDefault();

                NewOrder.OrderDetails.Add(NewOrderDetail1);
            }

            if (txtQuantity2.Text != "")
            {
                int ProductID2 = Convert.ToInt32(ddlProduct1.SelectedValue);

                OrderDetail NewOrderDetail2 =
                    new OrderDetail();
                NewOrderDetail2.Quantity =
                    Convert.ToInt32(txtQuantity2.Text);
                NewOrderDetail2.Product =
                    colProducts.Where(x => x.Id == ProductID2)
                        .FirstOrDefault();

                NewOrder.OrderDetails.Add(NewOrderDetail2);
            }

            if (txtQuantity3.Text != "")
            {
                int ProductID3 = Convert.ToInt32(ddlProduct1.SelectedValue);

                OrderDetail NewOrderDetail3 =
                    new OrderDetail();
                NewOrderDetail3.Quantity =
                    Convert.ToInt32(txtQuantity3.Text);
                NewOrderDetail3.Product =
                    colProducts.Where(x => x.Id == ProductID3)
                        .FirstOrDefault();

                NewOrder.OrderDetails.Add(NewOrderDetail3);
            }

            if (txtQuantity4.Text != "")
            {
                int ProductID4 = Convert.ToInt32(ddlProduct1.SelectedValue);

                OrderDetail NewOrderDetail4 =
                    new OrderDetail();
                NewOrderDetail4.Quantity =
                    Convert.ToInt32(txtQuantity4.Text);
                NewOrderDetail4.Product =
                   colProducts.Where(x => x.Id == ProductID4)
                       .FirstOrDefault();

                NewOrder.OrderDetails.Add(NewOrderDetail4);
            }

            // Set-up a ChangeSet for the insert
            var ChangeSetEntry = new ChangeSetEntry(0, NewOrder, null, DomainOperation.Insert);
            var ChangeSetChanges = new ChangeSet(new ChangeSetEntry[] { ChangeSetEntry });

            // Perform the insert
            ds.Submit(ChangeSetChanges);

            // Display errors if any
            if (ChangeSetChanges.HasError)
            {
                foreach (var item in ChangeSetChanges.ChangeSetEntries)
                {
                    if (item.HasError)
                    {
                        foreach (var error in item.ValidationErrors)
                        {
                            lblOrderError.Text = lblOrderError.Text + "<br>" + error.Message;
                        }
                    }
                }
            }
            else
            {
                lblOrderError.Text = "Success";
            }
        }
        catch (Exception ex)
        {
            lblOrderError.Text = ex.Message;
        }
    }

Use At Your Own Risk

I would like to thank Karol and Xin of Microsoft for their assistance in creating this code, however, Microsoft wanted me to make sure that I made it very clear that this is unsupported. The Lightswitch team only supports calling LightSwitch code from the Silverlight Lightswitch client. What this means:

  1. Don't call Microsoft for support if you have problems getting this working
  2. It is possible that a future service pack could disable something
  3. In LightSwitch version 2 (and beyond) they may decide to make their own HTML page output, or make a lot of changes that would break this code.

My personal opinion (follow at your own risk)

  1. I will still use the LightSwitch forums for help as needed.
  2. I can write code that talks directly to the SQL server tables (you would have to code the business rules manually)
  3. You don't have to upgrade the project to LightSwitch 2 (and beyond).

I think I understand why they won't officially support this scenario. Number one, if a security issue arose they want the ability to control both ends of the data flow, and they can't promise to support code written on the client side that they have no way of knowing what it is. Also, they want to leave their options open in future versions of LightSwitch.

Because I know I can always code directly against the SQL server data, the risk wont keep me up at night.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"