Walkthrough: Creating a Data Source Extension

This walkthrough demonstrates how to create a data source extension for LightSwitch. A data source extension uses a custom domain service adapter class to work with other data sources. This enables you to access almost any data that is not in a data source natively supported by LightSwitch.

For extensibility, LightSwitch supports calling into a custom DomainService class as a kind of in-memory data adapter. LightSwitch calls the instance directly from its data service implementation to perform query and submit operations. The custom domain service is not exposed as a public service; therefore the [EnableClientAccess] attribute should not be applied. Using this mechanism, you can create a DomainService class that exposes entity types and implements the prescribed query, insert, update, and delete methods. LightSwitch infers a LightSwitch entity model based on the exposed entity types and infers an entity set based on the presence of a “default” query.

Creating a data source extension involves the following tasks:

  • Create a data source extension project.

  • Implement the data source class.

  • Create product and product category classes.

  • Test the data source extension.

Prerequisites

  • Visual Studio 2013 Professional

  • Visual Studio 2013 SDK

  • LightSwitch Extensibility Toolkit for Visual Studio 2013

Create a Data Source Extension Project

The first step is to create a project and add a LightSwitch Data Source template.

To create an extension project

  1. On the menu bar in Visual Studio, choose File, New, Project.

  2. In the New Project dialog box, expand the Visual Basic or Visual C# node, expand the LightSwitch node, choose the Extensibility node, and then choose the LightSwitch Extension Library template.

  3. In the Name field, enter DataSourceExtension. This extension library name will appear on the Extensions tab of the LightSwitch Application Designer.

  4. Choose the OK button to create a solution that contains the seven projects that are required for the extension.

To choose an extension type

  1. In Solution Explorer, choose the DataSourceExtension.Lspkg project.

  2. On the menu bar, choose Project, Add New Item.

  3. In the Add New Item dialog box, choose LightSwitch Data Source.

  4. In the Name field, enter XMLDataSource. This extension name will appear in the LightSwitch Attach Data Source Wizard.

  5. Choose the OK button. An XMLDataSource.vb or XMLDataSource.cs file will be added to the DataSourceExtension.Server project in your solution.

Implement the Data Source Class

Most of the logic for your data source extension is contained in the XMLDataSource class that is created for you in the XMLDataSource.vb or XMLDataSource.cs file. The necessary references and Imports or using statements have already been added to the project. For an XML data source, you must add some more namespace references.

To add namespace references

  • In Solution Explorer, open the XMLDataSource.vb or XMLDataSource.cs file in the DataSourceExtension.Server project. Add the following Imports or using statements at the top of the file.

    Imports System.Xml.Linq
    Imports System.Configuration
    Imports System.Web.Configuration
    
    using System.Xml.Linq;
    using System.Configuration;
    using System.Web.Configuration;
    

Next, add a Description attribute to provide a description in the LightSwitch Attach Data Source Wizard.

To add a description attribute

  • Add the following just after the // TODO: Create methods containing your application logic. comment line.

    'The description attribute will be displayed in the LightSwitch data attach wizard.
        <Description("Please provide the path to the XML file.")> _
    
    //The description attribute will be displayed in the LightSwitch data attach wizard.
        [Description("Please provide the path to the XML file.")]
    

Next, specify the namespace.

To specify the namespace

  • Replace the existing namespace declaration with the following.

    Namespace DataSourceExtension.DataSources
    
    namespace DataSourceExtension.DataSources
    

Next, add code to set the file path and connection string.

To implement the initialize method

  • The following code retrieves the file name and connection string from the web.config file, verifies the file, and adds the XML elements if doing so is required. Add this code to the XMLDataSource class.

    Private _db As XElement
            Private _filePath As String
    
            Public Overrides Sub Initialize(context As DomainServiceContext)
                MyBase.Initialize(context)
    
                'Get the file path from config.
                If WebConfigurationManager.ConnectionStrings(GetType(XMLDataSource).FullName) IsNot Nothing Then
                    _filePath = WebConfigurationManager.ConnectionStrings(GetType(XMLDataSource).FullName).ConnectionString
                End If
    
                If [String].IsNullOrWhiteSpace(_filePath) Then
                    Throw New Exception("The filepath must be provided in the web.config in the connection strings section under " & GetType(XMLDataSource).FullName)
                Else
                    If Not System.IO.File.Exists(_filePath) Then
                        Dim x As New XElement("DataRoot")
                        x.Save(_filePath)
                    End If
    
                    _db = XElement.Load(_filePath)
    
                    'Verify the file.
                    If _db.Name <> "DataRoot" Then
                        Throw New Exception("Corrupt file.")
                    End If
    
                    'Add a product categories node if one does not exist.
                    If _db.Element("ProductCategories") Is Nothing Then
                        _db.Add(New XElement("ProductCategories"))
                    End If
    
                    'Add a products node if one does not exist.
                    If _db.Element("Products") Is Nothing Then
                        _db.Add(New XElement("Products"))
                    End If
                End If
            End Sub
    
    private XElement _db;
            private string _filePath;
    
            public override void Initialize(DomainServiceContext context)
            {
                base.Initialize(context);
    
                //Get the file path from config.
                if (WebConfigurationManager.ConnectionStrings[typeof(XMLDataSource).FullName] != null)
                    _filePath = WebConfigurationManager.ConnectionStrings[typeof(XMLDataSource).FullName].ConnectionString;
    
                if (String.IsNullOrWhiteSpace(_filePath))
                {
                    throw new Exception("The filepath must be provided in the web.config in the connection strings section under " + typeof(XMLDataSource).FullName);
                }
                else
                {
                    if (!System.IO.File.Exists(_filePath))
                    {
                        XElement x = new XElement("DataRoot");
                        x.Save(_filePath);
                    }
    
                    _db = XElement.Load(_filePath);
    
                    //Verify the file.
                    if (_db.Name != "DataRoot")
                        throw new Exception("Corrupt file.");
    
                    //Add a product categories node if one does not exist.
                    if (_db.Element("ProductCategories") == null)
                        _db.Add(new XElement("ProductCategories"));
    
                    //Add a products node if one does not exist.
                    if (_db.Element("Products") == null)
                        _db.Add(new XElement("Products"));
                }
            }
    

Next, implement the Submit method to save changes to the file.

To implement the submit method

  • Add the following code to the XMLDataSource class.

    Public Overrides Function Submit(changeSet As ChangeSet) As Boolean
                Dim c As New ChangeSet(changeSet.ChangeSetEntries.OrderBy(Function(entry) entry.Entity, New ProductEntitiesComparer()))
    
                Dim baseResult As [Boolean] = MyBase.Submit(changeSet)
                If baseResult Then
                    _db.Save(_filePath)
                    Return True
                Else
                    Return False
                End If
            End Function
    
    public override bool Submit(ChangeSet changeSet)
            {
                ChangeSet c = new ChangeSet(changeSet.ChangeSetEntries.OrderBy(entry => entry.Entity, new ProductEntitiesComparer()));
    
                Boolean baseResult = base.Submit(changeSet);
                if (baseResult)
                {
                    _db.Save(_filePath);
                    return true;
                }
                else
                    return false;
            }
    

    Note

    You will see an error at this point because the ProductEntitiesComparer class is not yet implemented. You can ignore this error; you will implement the class later.

Next, implement two default query methods in the class; one for products and one for product categories.

To implement query methods

  • Add a new region to the XMLDataSource class.

    #Region "Queries"
    
    #End Region
    
    #region Queries
    
            #endregion
    
  • Add the following code to the Queries region.

    Protected Overrides Function Count(Of T)(query As IQueryable(Of T)) As Integer
                Return query.Count()
            End Function
    
            <Query(IsDefault:=True)> _
            Public Function GetProducts() As IQueryable(Of Product)
                Dim products As New List(Of Product)()
                For Each pElem As XElement In _db.Descendants("Product")
                    products.Add(GetProduct(pElem))
                Next
                Return products.AsQueryable()
            End Function
    
            <Query(IsDefault:=True)> _
            Public Function GetProductCategories() As IQueryable(Of ProductCategory)
                Dim categories As New List(Of ProductCategory)()
                For Each catElem As XElement In _db.Descendants("ProductCategory")
                    categories.Add(GetProductCategory(catElem))
                Next
    
                Return categories.AsQueryable()
            End Function
    
    protected override int Count<T>(IQueryable<T> query)
            {
                return query.Count();
            }
    
            [Query(IsDefault = true)]
            public IQueryable<Product> GetProducts()
            {
                List<Product> products = new List<Product>();
                foreach (XElement pElem in _db.Descendants("Product"))
                {
                    products.Add(GetProduct(pElem));
                }
                return products.AsQueryable();
            }
    
            [Query(IsDefault = true)]
            public IQueryable<ProductCategory> GetProductCategories()
            {
                List<ProductCategory> categories = new List<ProductCategory>();
                foreach (XElement catElem in _db.Descendants("ProductCategory"))
                {
                    categories.Add(GetProductCategory(catElem));
                }
    
                return categories.AsQueryable();
            }
    

    Note

    You will see more errors at this point. You can ignore the errors; as soon as all of the code for the walkthrough has been added, the errors will be resolved.

Next, you will implement two custom queries. These will appear in the Data Sources node in LightSwitch at design time.

To implement custom queries

  1. Add a CustomQueries region to the XMLDataSource class.

    #Region "CustomQueries"
    
    #End Region
    
    #region CustomQueries
    
    #endregion
    
  2. Add the following code to the CustomQueries region.

    'All query parameters need to be nullable. 
            Public Function GetProductsByCategory(categoryID As Nullable(Of Guid)) As IQueryable(Of Product)
                Dim products As New List(Of Product)()
    
                'Return no products if categoryID is as null.
                If categoryID.HasValue Then
                    For Each pElem As XElement In _db.Descendants("Product")
                        Dim p As Product = GetProduct(pElem)
                        If p.CategoryID = categoryID.Value Then
                            products.Add(p)
                        End If
                    Next
                End If
                Return products.AsQueryable()
            End Function
    
            'Query that returns a single item.
            'Mark queries that return as single item as composable = false.
            <Query(IsComposable:=False)> _
            Public Function GetProductByID(productID As Nullable(Of Guid)) As Product
                Dim product As Product = Nothing
    
                'Only return a result if you are passed a value.
                If productID.HasValue Then
                    Dim pElem As XElement = (From p In _db.Descendants("Product") Where p.Element("ProductID").Value = productID.Value.ToString() Select p).FirstOrDefault()
                    If pElem IsNot Nothing Then
                        product = GetProduct(pElem)
                    End If
                End If
                Return product
    
            End Function
    
    //All query parameters need to be nullable. 
            public IQueryable<Product> GetProductsByCategory(Nullable<Guid> categoryID)
            {
                List<Product> products = new List<Product>();
    
                //Return no products if categoryID is as null.
                if (categoryID.HasValue)
                {
                    foreach (XElement pElem in _db.Descendants("Product"))
                    {
                        Product p = GetProduct(pElem);
                        if (p.CategoryID == categoryID.Value)
                        {
                            products.Add(p);
                        }
                    }
                }
                return products.AsQueryable();
            }
    
            //Query that returns a single item.
            //Mark queries that return as single item as composable = false.
            [Query(IsComposable = false)]
            public Product GetProductByID(Nullable<Guid> productID)
            {
                Product product = null;
    
                //Only return a result if you are passed a value.
                if (productID.HasValue)
                {
                    XElement pElem = (from XElement p in _db.Descendants("Product")
                                      where p.Element("ProductID").Value == productID.Value.ToString()
                                      select p).FirstOrDefault();
                    if (pElem != null)
                    {
                        product = GetProduct(pElem);
                    }
                }
                return product;
    
            }
    

Next, define Update, Delete, and Insert methods for the product categories.

To add update, delete, and insert methods

  1. Add a Category Update/Delete/Insert Methods region to the XMLDataSource class.

    #Region "Category Update/Delete/Insert Methods"
    
    #End Region
    
    #region Category Update/Delete/Insert Methods
    
    #endregion
    
  2. Add the following code to the Category Update/Delete/Insert Methods region.

    Public Sub InsertProductCategory(pc As ProductCategory)
                Try
                    pc.CategoryID = Guid.NewGuid()
                    Dim catElem As XElement = GetProductCategoryElem(pc)
    
                    'Update the category ID on any related product entities.
                    For Each e As ChangeSetEntry In ChangeSet.ChangeSetEntries
                        If TypeOf e.Entity Is Product Then
                            If CObj(e.Entity).Category = CObj(pc) Then
                                DirectCast(e.Entity, Product).CategoryID = pc.CategoryID
                            End If
                        End If
                    Next
    
    
                    'Update the xml doc.
                    _db.Element("ProductCategories").Add(catElem)
                Catch ex As Exception
                    Throw New Exception("Error inserting ProductCategory " & Convert.ToString(pc.CategoryName), ex)
                End Try
            End Sub
    
            Public Sub UpdateProductCategory(pc As ProductCategory)
                Try
                    'Get existing item from the XML db.
                    Dim storeCatElem As XElement = (From c In _db.Descendants("ProductCategory") Where c.Element("CategoryID").Value = pc.CategoryID.ToString() Select c).FirstOrDefault()
    
                    If storeCatElem Is Nothing Then
                        'Category does not exist.  Indicate that item has already been deleted.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(pc) Select e).First()
                        entry.IsDeleteConflict = True
                    Else
                        Dim storeCategory As ProductCategory = GetProductCategory(storeCatElem)
    
                        'Find entry in the changeset to compare original values.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(pc) Select e).First()
                        'List of conflicting fields.
                        Dim conflictMembers As New List(Of [String])()
    
                        If storeCategory.CategoryName <> DirectCast(entry.OriginalEntity, ProductCategory).CategoryName Then
                            conflictMembers.Add("CategoryName")
                        End If
                        If storeCategory.Description <> DirectCast(entry.OriginalEntity, ProductCategory).Description Then
                            conflictMembers.Add("Description")
                        End If
    
                        'Set conflict members.
                        entry.ConflictMembers = conflictMembers
                        entry.StoreEntity = storeCategory
    
                        If conflictMembers.Count < 1 Then
                            'Update the xml _db.
                            storeCatElem.ReplaceWith(GetProductCategoryElem(pc))
                        End If
                    End If
                Catch ex As Exception
                    Throw New Exception("Error updating Category " & pc.CategoryID.ToString() & ":" & Convert.ToString(pc.CategoryName), ex)
                End Try
            End Sub
    
            Public Sub DeleteProductCategory(pc As ProductCategory)
                Try
                    Dim storeCatElem As XElement = (From c In _db.Descendants("ProductCategory") Where c.Element("CategoryID").Value = pc.CategoryID.ToString() Select c).FirstOrDefault()
    
                    If storeCatElem Is Nothing Then
                        'Category does not exist.  Indicate that item has already been deleted.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(pc) Select e).First()
                        entry.IsDeleteConflict = True
                    Else
                        storeCatElem.Remove()
                    End If
                Catch ex As Exception
                    Throw New Exception("Error deleting Category " & pc.CategoryID.ToString() & ":" & Convert.ToString(pc.CategoryName), ex)
                End Try
            End Sub
    
    public void InsertProductCategory(ProductCategory pc)
            {
                try
                {
                    pc.CategoryID = Guid.NewGuid();
                    XElement catElem = GetProductCategoryElem(pc);
    
                    //Update the category ID on any related product entities.
                    foreach (ChangeSetEntry e in ChangeSet.ChangeSetEntries)
                    {
                        if (e.Entity is Product)
                        {
                            if (((Product)e.Entity).Category == pc)
                            {
                                ((Product)e.Entity).CategoryID = pc.CategoryID;
                            }
                        }
                    }
    
    
                    //Update the xml doc.
                    _db.Element("ProductCategories").Add(catElem);
                }
                catch (Exception ex)
                {
                    throw new Exception("Error inserting ProductCategory " + pc.CategoryName, ex);
                }
            }
    
            public void UpdateProductCategory(ProductCategory pc)
            {
                try
                {
                    //Get existing item from the XML db.
                    XElement storeCatElem =
                        (from c in _db.Descendants("ProductCategory")
                         where c.Element("CategoryID").Value == pc.CategoryID.ToString()
                         select c).FirstOrDefault();
    
                    if (storeCatElem == null)
                    {
                        //Category does not exist.  Indicate that the item has already been deleted.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == pc
                             select e).First();
                        entry.IsDeleteConflict = true;
                    }
                    else
                    {
                        ProductCategory storeCategory = GetProductCategory(storeCatElem);
    
                        //Find entry in the changeset to compare original values.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == pc
                             select e).First();
    
                        //List of conflicting fields.
                        List<String> conflictMembers = new List<String>();
    
                        if (storeCategory.CategoryName != ((ProductCategory)entry.OriginalEntity).CategoryName)
                            conflictMembers.Add("CategoryName");
                        if (storeCategory.Description != ((ProductCategory)entry.OriginalEntity).Description)
                            conflictMembers.Add("Description");
    
                        //Set conflict members>
                        entry.ConflictMembers = conflictMembers;
                        entry.StoreEntity = storeCategory;
    
                        if (conflictMembers.Count < 1)
                        {
                            //Update the xml _db.
                            storeCatElem.ReplaceWith(GetProductCategoryElem(pc));
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error updating Category " + pc.CategoryID.ToString() + ":" + pc.CategoryName, ex);
                }
            }
    
            public void DeleteProductCategory(ProductCategory pc)
            {
                try
                {
                    XElement storeCatElem =
                        (from c in _db.Descendants("ProductCategory")
                         where c.Element("CategoryID").Value == pc.CategoryID.ToString()
                         select c).FirstOrDefault();
    
                    if (storeCatElem == null)
                    {
                        //Category does not exist.  Indicate that the item has already been deleted.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == pc
                             select e).First();
                        entry.IsDeleteConflict = true;
                    }
                    else
                    {
                        storeCatElem.Remove();
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error deleting Category " + pc.CategoryID.ToString() + ":" + pc.CategoryName, ex);
                }
            }
    

Next, implement a similar set of methods for products.

To add methods

  1. Add a Product Update/Delete/Insert Methods region to the XMLDataSource class.

    #Region "Product Update/Delete/Insert Methods"
    
    #End Region
    
    #region Product Update/Delete/Insert Methods
    
    #endregion
    
  2. Add the following code to the Product Update/Delete/Insert Methods region.

    Public Sub InsertProduct(p As Product)
                Try
                    p.ProductID = Guid.NewGuid()
                    Dim productElem As XElement = GetProductElem(p)
                    _db.Element("Products").Add(productElem)
                Catch ex As Exception
                    Throw New Exception("Error inserting Product " & Convert.ToString(p.ProductName), ex)
                End Try
            End Sub
    
            Public Sub UpdateProduct(p As Product)
                Try
                    Dim storeProductElem As XElement = (From c In _db.Descendants("Product") Where c.Element("ProductID").Value = p.ProductID.ToString() Select c).FirstOrDefault()
    
                    If storeProductElem Is Nothing Then
                        'Product does not exist.  Indicate that the item has already been deleted.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(p) Select e).First()
                        entry.IsDeleteConflict = True
                    Else
                        Dim storeProduct As Product = GetProduct(storeProductElem)
    
                        'Find the entry in the changeset to compare original values.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(p) Select e).First()
    
                        'List of conflicting fields.
                        Dim conflictMembers As New List(Of [String])()
    
                        If storeProduct.ProductName <> DirectCast(entry.OriginalEntity, Product).ProductName Then
                            conflictMembers.Add("ProductName")
                        End If
                        If storeProduct.CategoryID <> DirectCast(entry.OriginalEntity, Product).CategoryID Then
                            conflictMembers.Add("CategoryID")
                        End If
                        If storeProduct.QuantityPerUnit <> DirectCast(entry.OriginalEntity, Product).QuantityPerUnit Then
                            conflictMembers.Add("QuantityPerUnit")
                        End If
                        If storeProduct.UnitPrice <> DirectCast(entry.OriginalEntity, Product).UnitPrice Then
                            conflictMembers.Add("UnitPrice")
                        End If
                        If storeProduct.UnitsInStock <> DirectCast(entry.OriginalEntity, Product).UnitsInStock Then
                            conflictMembers.Add("UnitsInStock")
                        End If
                        If storeProduct.ReorderLevel <> DirectCast(entry.OriginalEntity, Product).ReorderLevel Then
                            conflictMembers.Add("ReorderLevel")
                        End If
                        If storeProduct.Discontinued <> DirectCast(entry.OriginalEntity, Product).Discontinued Then
                            conflictMembers.Add("Discontinued")
                        End If
    
                        'Set conflict members.
                        entry.ConflictMembers = conflictMembers
                        entry.StoreEntity = storeProduct
    
                        If conflictMembers.Count < 1 Then
                            'Update the xml _db.
                            storeProductElem.ReplaceWith(GetProductElem(p))
                        End If
                    End If
                Catch ex As Exception
                    Throw New Exception("Error updating Product " & p.ProductID.ToString() & ":" & Convert.ToString(p.ProductName), ex)
                End Try
            End Sub
    
            Public Sub DeleteProduct(p As Product)
                Try
                    Dim storeProductElem As XElement = (From c In _db.Descendants("Product") Where c.Element("ProductID").Value = p.ProductID.ToString() Select c).FirstOrDefault()
    
                    If storeProductElem Is Nothing Then
                        'Product does not exist.  Indicate that the item has already been deleted.
                        Dim entry As ChangeSetEntry = (From e In ChangeSet.ChangeSetEntries Where e.Entity = CObj(p) Select e).First()
                        entry.IsDeleteConflict = True
                    Else
                        'Remove it.
                        storeProductElem.Remove()
                    End If
                Catch ex As Exception
                    Throw New Exception("Error deleting Product " & p.ProductID.ToString() & ":" & Convert.ToString(p.ProductName), ex)
                End Try
            End Sub
    
    public void InsertProduct(Product p)
            {
                try
                {
                    p.ProductID = Guid.NewGuid();
                    XElement productElem = GetProductElem(p);
                    _db.Element("Products").Add(productElem);
                }
                catch (Exception ex)
                {
                    throw new Exception("Error inserting Product " + p.ProductName, ex);
                }
            }
    
            public void UpdateProduct(Product p)
            {
                try
                {
                    XElement storeProductElem =
                        (from c in _db.Descendants("Product")
                         where c.Element("ProductID").Value == p.ProductID.ToString()
                         select c).FirstOrDefault();
    
                    if (storeProductElem == null)
                    {
                        //Product does not exist.  Indicate that the item has already been deleted.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == p
                             select e).First();
                        entry.IsDeleteConflict = true;
                    }
                    else
                    {
                        Product storeProduct = GetProduct(storeProductElem);
    
                        //Find the entry in the changeset to compare original values.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == p
                             select e).First();
    
                        //List of conflicting fields.
                        List<String> conflictMembers = new List<String>();
    
                        if (storeProduct.ProductName != ((Product)entry.OriginalEntity).ProductName)
                            conflictMembers.Add("ProductName");
                        if (storeProduct.CategoryID != ((Product)entry.OriginalEntity).CategoryID)
                            conflictMembers.Add("CategoryID");
                        if (storeProduct.QuantityPerUnit != ((Product)entry.OriginalEntity).QuantityPerUnit)
                            conflictMembers.Add("QuantityPerUnit");
                        if (storeProduct.UnitPrice != ((Product)entry.OriginalEntity).UnitPrice)
                            conflictMembers.Add("UnitPrice");
                        if (storeProduct.UnitsInStock != ((Product)entry.OriginalEntity).UnitsInStock)
                            conflictMembers.Add("UnitsInStock");
                        if (storeProduct.ReorderLevel != ((Product)entry.OriginalEntity).ReorderLevel)
                            conflictMembers.Add("ReorderLevel");
                        if (storeProduct.Discontinued != ((Product)entry.OriginalEntity).Discontinued)
                            conflictMembers.Add("Discontinued");
    
                        //Set conflict members.
                        entry.ConflictMembers = conflictMembers;
                        entry.StoreEntity = storeProduct;
    
                        if (conflictMembers.Count < 1)
                        {
                            //Update the xml _db.
                            storeProductElem.ReplaceWith(GetProductElem(p));
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error updating Product " + p.ProductID.ToString() + ":" + p.ProductName, ex);
                }
            }
    
            public void DeleteProduct(Product p)
            {
                try
                {
                    XElement storeProductElem =
                        (from c in _db.Descendants("Product")
                         where c.Element("ProductID").Value == p.ProductID.ToString()
                         select c).FirstOrDefault();
    
                    if (storeProductElem == null)
                    {
                        //The product does not exist.  Indicate that the item has already been deleted.
                        ChangeSetEntry entry =
                            (from e in ChangeSet.ChangeSetEntries
                             where e.Entity == p
                             select e).First();
                        entry.IsDeleteConflict = true;
                    }
                    else
                    {
                        //Remove it.
                        storeProductElem.Remove();
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error deleting Product " + p.ProductID.ToString() + ":" + p.ProductName, ex);
                }
            }
    

Next, add some helper functions.

To implement helper functions

  • Add a new region to the XMLDataSource class.

    #Region “HelperFunctions”
    
    #End Region
    
    #region HelperFunctions
    
    #endregion
    
  • Add the following code to the Queries region.

    Private Function GetProductElem(p As Product) As XElement
                Dim productElem As New XElement("Product", New XElement("ProductID", New Object() {New XAttribute("DataType", "Guid"), p.ProductID}), New XElement("ProductName", New Object() {New XAttribute("DataType", "String"), p.ProductName}), New XElement("CategoryID", New Object() {New XAttribute("DataType", "Guid"), p.CategoryID}), New XElement("QuantityPerUnit", New Object() {New XAttribute("DataType", "String"), p.QuantityPerUnit}), New XElement("UnitPrice", New Object() {New XAttribute("DataType", "Decimal"), p.UnitPrice}), _
                 New XElement("UnitsInStock", New Object() {New XAttribute("DataType", "Int32"), p.UnitsInStock}), New XElement("ReorderLevel", New Object() {New XAttribute("DataType", "Int32?"), p.ReorderLevel}), New XElement("Discontinued", New Object() {New XAttribute("DataType", "Boolean"), p.Discontinued}))
                Return productElem
            End Function
    
            Private Function GetProduct(pElem As XElement) As Product
                Dim p As New Product()
    
                p.ProductID = New Guid(pElem.Element("ProductID").Value)
                p.ProductName = pElem.Element("ProductName").Value
                p.CategoryID = New Guid(pElem.Element("CategoryID").Value)
                p.QuantityPerUnit = pElem.Element("QuantityPerUnit").Value
                p.UnitPrice = Decimal.Parse(pElem.Element("UnitPrice").Value)
                p.UnitsInStock = Int32.Parse(pElem.Element("UnitsInStock").Value)
                If Not [String].IsNullOrWhiteSpace(pElem.Element("ReorderLevel").Value) Then
                    p.ReorderLevel = Int32.Parse(pElem.Element("ReorderLevel").Value)
                End If
                p.Discontinued = [Boolean].Parse(pElem.Element("Discontinued").Value)
    
                Return p
            End Function
    
            Private Function GetProductCategory(catElem As XElement) As ProductCategory
                Dim pc As New ProductCategory()
                pc.CategoryID = New Guid(catElem.Element("CategoryID").Value)
                pc.CategoryName = catElem.Element("CategoryName").Value
                pc.Description = catElem.Element("Description").Value
    
                Return pc
            End Function
    
            Private Function GetProductCategoryElem(pc As ProductCategory) As XElement
                Dim catElem As New XElement("ProductCategory", New XElement("CategoryID", New Object() {New XAttribute("DataType", "Guid"), pc.CategoryID}), New XElement("CategoryName", New Object() {New XAttribute("DataType", "String"), pc.CategoryName}), New XElement("Description", New Object() {New XAttribute("DataType", "String"), pc.Description}))
                Return catElem
            End Function
    
    private XElement GetProductElem(Product p)
            {
                XElement productElem = new XElement("Product",
                    new XElement("ProductID", new object[] { new XAttribute("DataType", "Guid"), p.ProductID }),
                    new XElement("ProductName", new object[] { new XAttribute("DataType", "String"), p.ProductName }),
                    new XElement("CategoryID", new object[] { new XAttribute("DataType", "Guid"), p.CategoryID }),
                    new XElement("QuantityPerUnit", new object[] { new XAttribute("DataType", "String"), p.QuantityPerUnit }),
                    new XElement("UnitPrice", new object[] { new XAttribute("DataType", "Decimal"), p.UnitPrice }),
                    new XElement("UnitsInStock", new object[] { new XAttribute("DataType", "Int32"), p.UnitsInStock }),
                    new XElement("ReorderLevel", new object[] { new XAttribute("DataType", "Int32?"), p.ReorderLevel }),
                    new XElement("Discontinued", new object[] { new XAttribute("DataType", "Boolean"), p.Discontinued })
                    );
                return productElem;
            }
    
            private Product GetProduct(XElement pElem)
            {
                Product p = new Product();
    
                p.ProductID = new Guid(pElem.Element("ProductID").Value);
                p.ProductName = pElem.Element("ProductName").Value;
                p.CategoryID = new Guid(pElem.Element("CategoryID").Value);
                p.QuantityPerUnit = pElem.Element("QuantityPerUnit").Value;
                p.UnitPrice = decimal.Parse(pElem.Element("UnitPrice").Value);
                p.UnitsInStock = Int32.Parse(pElem.Element("UnitsInStock").Value);
                if (!String.IsNullOrWhiteSpace(pElem.Element("ReorderLevel").Value))
                {
                    p.ReorderLevel = Int32.Parse(pElem.Element("ReorderLevel").Value);
                }
                p.Discontinued = Boolean.Parse(pElem.Element("Discontinued").Value);
    
                return p;
            }
    
            private ProductCategory GetProductCategory(XElement catElem)
            {
                ProductCategory pc = new ProductCategory();
                pc.CategoryID = new Guid(catElem.Element("CategoryID").Value);
                pc.CategoryName = catElem.Element("CategoryName").Value;
                pc.Description = catElem.Element("Description").Value;
    
                return pc;
            }
    
            private XElement GetProductCategoryElem(ProductCategory pc)
            {
                XElement catElem = new XElement("ProductCategory",
                   new XElement("CategoryID", new object[] { new XAttribute("DataType", "Guid"), pc.CategoryID }),
                   new XElement("CategoryName", new object[] { new XAttribute("DataType", "String"), pc.CategoryName }),
                   new XElement("Description", new object[] { new XAttribute("DataType", "String"), pc.Description })
                   );
                return catElem;
            }
    

Next, add a Comparer class to the XMLDataSource.vb or XMLDataSource.cs file.

To add the comparer class

  • The following code retrieves the file name and connection string from the web.config file, verifies the file, and adds the XML elements if it is required. Add the code after the XMLDataSource class, inside the DataSourceExtension.DataSources namespace.

    Public Class ProductEntitiesComparer
            Inherits Comparer(Of Object)
            Public Overrides Function Compare(x As Object, y As Object) As Integer
                If TypeOf x Is Product AndAlso TypeOf y Is ProductCategory Then
                    Return 1
                ElseIf TypeOf x Is ProductCategory AndAlso TypeOf y Is Product Then
                    Return -1
                Else
                    Return 0
                End If
            End Function
        End Class
    
    public class ProductEntitiesComparer : Comparer<object>
        {
            public override int Compare(object x, object y)
            {
                if (x is Product && y is ProductCategory)
                    return 1;
                else if (x is ProductCategory && y is Product)
                    return -1;
                else
                    return 0;
            }
        }
    

These steps conclude the XMLDataSource class. The code in this class not only has service connection related code, but also two queries, one to retrieve product by ID and another to get product by category. These queries can be used in a LightSwitch application as well.

The next step is to create two classes to store the Product and ProductCategory data.

Create Product and Product Category Classes

In addition to the XMLDataSource class, you will also need two more classes to represent the Product and ProductCategory entities. These two classes will be added to the DataSourceExtension.Server project.

To add the Product class

  1. In Solution Explorer, choose the DataSourceExtension.Server project.

  2. On the Project menu, choose Add Class.

  3. In the Add New Item dialog box, choose the Name field, type Product, and then choose the Add button.

  4. In the Product file, replace the existing contents with the following code.

    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Text
    Imports System.ComponentModel
    Imports System.ComponentModel.DataAnnotations
    
    
    Namespace DataSourceExtension
        Public Class Product
            <Key()> _
            <[ReadOnly](True)> _
            <Display(Name:="Product ID")> _
            <ScaffoldColumn(False)> _
            Public Property ProductID() As Guid
                Get
                    Return m_ProductID
                End Get
                Set(value As Guid)
                    m_ProductID = value
                End Set
            End Property
            Private m_ProductID As Guid
    
            <Required()> _
            <Display(Name:="Product Name")> _
            Public Property ProductName() As String
                Get
                    Return m_ProductName
                End Get
                Set(value As String)
                    m_ProductName = value
                End Set
            End Property
            Private m_ProductName As String
    
            <Required()> _
            <Display(Name:="Category ID")> _
            Public Property CategoryID() As Guid
                Get
                    Return m_CategoryID
                End Get
                Set(value As Guid)
                    m_CategoryID = value
                End Set
            End Property
            Private m_CategoryID As Guid
    
            <Display(Name:="Quantity Per Unit")> _
            Public Property QuantityPerUnit() As String
                Get
                    Return m_QuantityPerUnit
                End Get
                Set(value As String)
                    m_QuantityPerUnit = value
                End Set
            End Property
            Private m_QuantityPerUnit As String
    
            <Range(0, Double.MaxValue, ErrorMessage:="The specified price must be greater than zero.")> _
            <Display(Name:="Unit Price")> _
            Public Property UnitPrice() As [Decimal]
                Get
                    Return m_UnitPrice
                End Get
                Set(value As [Decimal])
                    m_UnitPrice = value
                End Set
            End Property
            Private m_UnitPrice As [Decimal]
    
            <Display(Name:="Units In Stock")> _
            <Range(0, Int32.MaxValue, ErrorMessage:="Cannot have a negative quantity of products.")> _
            Public Property UnitsInStock() As Int32
                Get
                    Return m_UnitsInStock
                End Get
                Set(value As Int32)
                    m_UnitsInStock = value
                End Set
            End Property
            Private m_UnitsInStock As Int32
    
            <Display(Name:="Reorder Level")> _
            Public Property ReorderLevel() As Nullable(Of Int32)
                Get
                    Return m_ReorderLevel
                End Get
                Set(value As Nullable(Of Int32))
                    m_ReorderLevel = value
                End Set
            End Property
            Private m_ReorderLevel As Nullable(Of Int32)
    
            <Display(Name:="Discontinued")> _
            Public Property Discontinued() As [Boolean]
                Get
                    Return m_Discontinued
                End Get
                Set(value As [Boolean])
                    m_Discontinued = value
                End Set
            End Property
            Private m_Discontinued As [Boolean]
    
            <Association("Product_Category", "CategoryID", "CategoryID", IsForeignKey:=True)> _
            <Display(Name:="Category")> _
            Public Property Category() As ProductCategory
                Get
                    Return m_Category
                End Get
                Set(value As ProductCategory)
                    m_Category = value
                End Set
            End Property
            Private m_Category As ProductCategory
        End Class
    End Namespace
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    
    
    namespace DataSourceExtension
    {
        public class Product
        {
            [Key()]
            [ReadOnly(true)]
            [Display(Name = "Product ID")]
            [ScaffoldColumn(false)]
            public Guid ProductID { get; set; }
    
            [Required()]
            [Display(Name = "Product Name")]
            public string ProductName { get; set; }
    
            [Required()]
            [Display(Name = "Category ID")]
            public Guid CategoryID { get; set; }
    
            [Display(Name = "Quantity Per Unit")]
            public string QuantityPerUnit { get; set; }
    
            [Range(0, double.MaxValue, ErrorMessage = "The specified price must be greater than zero.")]
            [Display(Name = "Unit Price")]
            public Decimal UnitPrice { get; set; }
    
            [Display(Name = "Units In Stock")]
            [Range(0, Int32.MaxValue, ErrorMessage = "Cannot have a negative quantity of products.")]
            public Int32 UnitsInStock { get; set; }
    
            [Display(Name = "Reorder Level")]
            public Nullable<Int32> ReorderLevel { get; set; }
    
            [Display(Name = "Discontinued")]
            public Boolean Discontinued { get; set; }
    
            [Association("Product_Category", "CategoryID", "CategoryID", IsForeignKey = true)]
            [Display(Name = "Category")]
            public ProductCategory Category { get; set; }
        }
    }
    

To add the ProductCategory class

  1. In Solution Explorer, choose the DataSourceExtension.Server project.

  2. On the Project menu, choose Add Class.

  3. In the Add New Item dialog box, choose the Name field, type ProductCategory, and then choose the Add button.

  4. In the ProductCategory file, replace the existing contents with the following code.

    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Text
    Imports System.ComponentModel
    Imports System.ComponentModel.DataAnnotations
    
    Namespace DataSourceExtension
        Public Class ProductCategory
            <[ReadOnly](True)> _
            <Key()> _
            <Display(Name:="Category ID")> _
            <ScaffoldColumn(False)> _
            Public Property CategoryID() As Guid
                Get
                    Return m_CategoryID
                End Get
                Set(value As Guid)
                    m_CategoryID = Value
                End Set
            End Property
            Private m_CategoryID As Guid
    
            <Required()> _
            <Display(Name:="Category Name")> _
            Public Property CategoryName() As String
                Get
                    Return m_CategoryName
                End Get
                Set(value As String)
                    m_CategoryName = Value
                End Set
            End Property
            Private m_CategoryName As String
    
            <Display(Name:="Description")> _
            Public Property Description() As [String]
                Get
                    Return m_Description
                End Get
                Set(value As [String])
                    m_Description = Value
                End Set
            End Property
            Private m_Description As [String]
    
            <Display(Name:="Products")> _
            <Association("Product_Category", "CategoryID", "CategoryID")> _
            Public Property Products() As ICollection(Of Product)
                Get
                    Return m_Products
                End Get
                Set(value As ICollection(Of Product))
                    m_Products = Value
                End Set
            End Property
            Private m_Products As ICollection(Of Product)
        End Class
    End Namespace
    
     using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    
    namespace DataSourceExtension
    {
        public class ProductCategory
        {
            [ReadOnly(true)]
            [Key()]
            [Display(Name = "Category ID")]
            [ScaffoldColumn(false)]
            public Guid CategoryID { get; set; }
    
            [Required()]
            [Display(Name = "Category Name")]
            public string CategoryName { get; set; }
    
            [Display(Name = "Description")]
            public String Description { get; set; }
    
            [Display(Name = "Products")]
            [Association("Product_Category", "CategoryID", "CategoryID")]
            public ICollection<Product> Products { get; set; }
        }
    }
    

These steps conclude the code for the data source extension. Now you can test it in an experimental instance of Visual Studio.

Test the Data Source Extension

You can test the data source extension in an experimental instance of Visual Studio. If you have not already tested another LightSwitch extensibility project, you have to enable the experimental instance first.

To enable an experimental instance

  1. In Solution Explorer, choose the BusinessTypeExtension.Vsix project.

  2. On the menu bar, choose Project, BusinessTypeExtension.Vsix Properties.

  3. On the Debug tab, under Start Action, choose Start external program.

  4. Enter the path of the Visual Studio executable, devenv.exe.

    By default on a 32-bit system, the path is C:\Program Files\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe; on a 64-bit system, it is C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe.

  5. In the Command line arguments field, enter /rootsuffix Exp.

    Note

    All subsequent LightSwitch extensibility projects will also use this setting, by default.

To test the data source extension

  1. On the menu bar, choose Debug, Start Debugging. An experimental instance of Visual Studio opens.

  2. In the experimental instance, on the menu bar, choose File, New, Project.

  3. In the New Project dialog box, expand the Visual Basic or Visual C# node, choose the LightSwitch node, and then choose the LightSwitch Desktop Application template.

  4. In the Name field, enter DataSourceTest, and then choose the OK button to create a test project.

  5. On the menu bar, choose Project, DataSourceTest Properties.

  6. In the project designer, on the Extensions tab, check the DataSourceExtension check box.

  7. On the menu bar, choose Project, Add Data Source.

  8. In the Attach Data Source wizard, choose WCF RIA Service, and then choose the Next button.

  9. Choose DataSourceExtension.DataSources.XMLDataSource, and then choose the Next button.

  10. Expand the Entities node, and then choose both Product and ProductCategory.

  11. In the Connection String field, type C:\Temp\Products.xml, and then choose the Finish button.

    The data source is added to the Data Sources node in Solution Explorer. Notice that both the ProductCategory and Products tables appear, and the GetProductByID and GetProductsByCategory queries appear under Products.

  12. Create an application that has screens that use the tables and the queries, and then run the application to observe the behavior.

    Notice that the data source functions just like any other data source. This allows you to add, delete, and update data.

Next Steps

This concludes the data source walkthrough; you should now have a fully functioning data source extension that you can reuse in any LightSwitch project. This was just one example of a data source; you might want to create a data source that accesses a different type of data. The same basic steps and principles apply.

If you are going to distribute your extension, there are a couple more steps that you should take. To make sure that the information displayed for your extension in the Project Designer and in Extension Manager is correct, you can update the properties for the VSIX package. For more information, see How to: Set VSIX Package Properties. In addition, there are several things that you should consider if you are going to distribute your extension publicly. For more information, see How to: Distribute a LightSwitch Extension.

See Also

Tasks

How to: Set VSIX Package Properties

How to: Distribute a LightSwitch Extension

Concepts

LightSwitch Extensibility Toolkit for Visual Studio 2013