DevTeach Day 2 - An Unexpected Turn
14 May 08 11:24 AM

Wednesday: I woke up this morning (yes it was still morning, I made it to bed before 3am, I think), feeling chipper and excited that I'd be able to spend the day attending sessions. I wasn't scheduled for any talks today, just tomorrow, so I figured this would be the perfect opportunity to learn something.

The first session I attended was an intro to F# that Ted Neward was giving. The talk was interesting, but I must admit I was a bit lost in the syntax at times. However, when Ted opened up ILDASM and showed the IL code that was being generated it started to click better for me. I dream in VB and OOP and F# is a purely functional language so it was a stretch.

 

As a former business systems architect, I was more interested in hearing where using F# in your application made the most sense. Ted's (unproven) hypothesis is that F# would make a good middle-tier and work well with REST-based applications. Hmmmm... interesting.

So now for the unexpected turn. In the beginning of the talk one of the other speakers, Etienne Tremblay, mentioned that Roy Osherove ended up in the hospital because he completely lost his voice and was feeling sick. (FEEL BETTER, ROY!) I jumped up and headed over to the conference organizer, Jean-Rene, and mentioned I could fill in if needed. He said he would let me know so I headed back into the F# session.

After the session I headed to lunch and JR came up to me and asked me to fill in for a LINQ talk. Roy's talk was called "LINQ to Anything - Building your own custom LINQ provider". I decided to do something a tad more mainstream and made up an abstract and titled it "LINQ to Everything" :-)

LINQ to Everything

In this session we will go over the major LINQ providers that shipped with Visual Studio 2008/.NET 3.5 and how to effectively use them in common business scenarios. We’ll go over LINQ to relational data using LINQ to DataSets and LINQ to SQL pointing out the benefits to each approach. We’ll also go over LINQ to XML and demonstrate some practical uses of creating, querying, transforming XML as well as how to take advantage of this technology with Office.

Sounds good to me! Now I'm working on a few demos while I'm sitting in a session on Silverlight 2.0 with Alan Griver because this room has a nice bunch of electrical outlets :-)

I'll report back and let you know how my LINQ talk goes. I just love filling in at the last minute. It keeps me on my toes.

Postedby Beth Massi | 0 Comments    
I Made it to Toronto! Day 0 and 1 at DevTeach
13 May 08 09:32 AM

Monday: I wake up at 3:15am CA time and get to SFO to make a 6am flight to Toronto. (Note: Air Canada flights do NOT leave from the international terminal, they leave from domestic terminal 3, go figure.) But the flight was pretty nice, TVs on the back of the seats and all that. Of course I spent most of the flight writing code.

I land in Toronto and get through customs about 3pm and arrive at the hotel around 4pm, quickly unpack my wrinkled dress shirts and get down to the lobby to meet Rob Windsor and crew. We take the subway to the Toronto user's group where I'm speaking with Scott Hanselman who's meeting us there. I've never spoken with Scott so we decide to break it up in half, I go over LINQ to SQL and LINQ to XML and Scott picks up with ADO.NET Data Services (Astoria).

It was a great talk. I created an app from scratch that pulled down RSS feeds (Scott's, Ted Neward and mine), inferred the schemas, enabled XML IntelliSense, and then stuffed the posts and related categories into a database using LINQ to SQL and XML in one query. Slick. Then I wrote some aggregate queries and showed off some of VB's expanded query syntax. Then I hooked up some validation rules and showed how to properly save and delete parent/child data. I also spent 2 minutes and dumped all the data into an Excel spreadsheet format using XML literals.

Then Scott showed the ins-and-outs of exposing your database through services and touched on a few of the gotchas. Great discussions!

Next stop was Party with Palermo, two chicken wings for dinner, then bed. (Oh, yea... we released VS08 SP1 Beta too).

Tuesday: I have the first session of the day. "What's New in VB 9" was the talk and I've done it many times before. So I tried something different this time. I tossed out my slides and decided to build another version of the same app I did the night before. But as I wrote the code I explained the language features at the same time. Usually I go over the language features one by one, and then show simple LINQ queries. I decided to do something different this time because, frankly, I think I was getting bored with my own talk. I almost always go out on a limb and build apps from scratch and I think people appreciate that, but sometimes it doesn't work if people don't have the fundamentals. I figured people had at least seen LINQ before so hopefully it worked for everyone.

I added the app to the WhatsNewVB CodeGallery project for you all to play with.

Enjoy!

Postedby Beth Massi | 0 Comments    
Quickly Changing Values of XML Elements Using LINQ
09 May 08 06:33 PM

I've had many questions lately on how you can query for a specific node in an XML document (or fragment) and change it's value using LINQ. (This must mean that people are really starting to use this stuff so I'm pretty excited.) This is really easy to do because you can modify the values of the selected XElements from your queries and that will change the source XML.

Here's an example:

Imports <xmlns="urn:mycompany:examples:plants">

Module Module1

    Sub Main()
        Dim plants = <?xml version="1.0" encoding="ISO-8859-1"?>
                     <CATALOG xmlns="urn:mycompany:examples:plants">
                         <PLANT>
                             <COMMON>Bloodroot</COMMON>
                             <BOTANICAL>Sanguinaria canadensis</BOTANICAL>
                             <ZONE>4</ZONE>
                             <LIGHT>Mostly Shady</LIGHT>
                             <PRICE>$2.44</PRICE>
                             <AVAILABILITY>031599</AVAILABILITY>
                         </PLANT>
                         <PLANT>
                             <COMMON>Columbine</COMMON>
                             <BOTANICAL>Aquilegia canadensis</BOTANICAL>
                             <ZONE>3</ZONE>
                             <LIGHT>Mostly Shady</LIGHT>
                             <PRICE>$9.37</PRICE>
                             <AVAILABILITY>030699</AVAILABILITY>
                         </PLANT>
                         <PLANT>
                             <COMMON>Marsh Marigold</COMMON>
                             <BOTANICAL>Caltha palustris</BOTANICAL>
                             <ZONE>4</ZONE>
                             <LIGHT>Mostly Sunny</LIGHT>
                             <PRICE>$6.81</PRICE>
                             <AVAILABILITY>051799</AVAILABILITY>
                         </PLANT>
                     </CATALOG>

        Dim q = From plant In plants...<PLANT> _
                Where plant.<COMMON>.Value = "Columbine" _
                Select plant

        For Each item In q
            q.<PRICE>.Value = "$49.99"
            q.<LIGHT>.Value = "Full Sun"
        Next

        plants.Save("plants.xml")
     
    End Sub

End Module

Couple things to note above, remember to import any namespaces being used in the XML otherwise your query will yield no results. And remember you can get XML IntelliSense if you import a schema (this is really easy, watch this). Of course, you can load the XML from a file (or URI) instead of using a literal and and get the same results.

Dim plants = XDocument.Load("plants.xml")

Dim q = From plant In plants...<PLANT> _
        Where plant.<COMMON>.Value = "Columbine" _
        Select plant

For Each item In q
    q.<PRICE>.Value = "$49.99"
    q.<LIGHT>.Value = "Full Sun"
Next

plants.Save("plants.xml")

In this example we're overwriting the source document, plants.xml, with our new values. Both examples produce this resulting XML:

<?xml version="1.0" encoding="iso-8859-1"?>
<CATALOG xmlns="urn:mycompany:examples:plants">
  <PLANT>
    <COMMON>Bloodroot</COMMON>
    <BOTANICAL>Sanguinaria canadensis</BOTANICAL>
    <ZONE>4</ZONE>
    <LIGHT>Mostly Shady</LIGHT>
    <PRICE>$2.44</PRICE>
    <AVAILABILITY>031599</AVAILABILITY>
  </PLANT>
  <PLANT>
    <COMMON>Columbine</COMMON>
    <BOTANICAL>Aquilegia canadensis</BOTANICAL>
    <ZONE>3</ZONE>
    <LIGHT>Full Sun</LIGHT>
    <PRICE>$49.99</PRICE>
    <AVAILABILITY>030699</AVAILABILITY>
  </PLANT>
  <PLANT>
    <COMMON>Marsh Marigold</COMMON>
    <BOTANICAL>Caltha palustris</BOTANICAL>
    <ZONE>4</ZONE>
    <LIGHT>Mostly Sunny</LIGHT>
    <PRICE>$6.81</PRICE>
    <AVAILABILITY>051799</AVAILABILITY>
  </PLANT>
</CATALOG>
Enjoy!
Postedby Beth Massi | 4 Comments    
Visual Basic "Learn" Section of MSDN - Give it a Spin
08 May 08 04:54 PM

The Learn tab of the Visual Basic Developer Center is being updated with a bunch of new content and VS 2008 topics. If you look at the center of the page you'll see the list of topics and when you click one, you should now see some fresh stuff. Currently there's over 200 items presented on the topic pages and we're adding more every week. Right now each of the topics are displayed in a fixed order by content type (i.e. Webcast, Video, Article, Blog, etc.) but we plan on adding a tag cloud for easier navigation and more community features going forward so check back often. You can also subscribe to each of the content sections independently by clicking the RSS icon next to each heading.

Or... if you don't like this view you can write your own query! That's right, these feeds are all dynamic and public. For instance, if you want to see all the Visual Basic items on LINQ:

http://services.community.microsoft.com/feeds/feed/query/tag/linq/eq/tag/visual%20basic/eq/and/locale/en-us/eq/and

Or maybe you want all the videos on data access in VB:

http://services.community.microsoft.com/feeds/feed/query/tag/video/eq/tag/data%20access/eq/and/tag/visual%20basic/eq/and/locale/en-us/eq/and 

Look for more features coming out soon.

Enjoy!

Postedby Beth Massi | 1 Comments    
Filed under: ,
Video Presentation: Conquering XML with LINQ in Visual Basic 9
30 April 08 05:44 PM

The session I did last November at QCon in San Fransisco is online so check it out. In this talk I introduced VB 9's LINQ to XML syntax and XML literals, axis properties, and embedded expressions.  

It's so weird watching myself but I think it was a pretty good presentation even though I was fighting a cough. I use my hands a lot (so unlike me <g>) but I think that helps express my points. That's my story and I'm sticking to it. ;-) I'll probably do even more hand waving at DevTeach in a couple weeks.

The code in the video is a bit hard to see, so if you prefer, I did a similar webcast earlier this month that you can view here.

Enjoy!

Postedby Beth Massi | 4 Comments    
I'm Speaking at DevTeach, Toronto...
28 April 08 09:14 AM

... and so are a bunch of other awesome speakers.

Join us all at DevTeach (first time in) Toronto, May 12th -16th. I've been speaking at DevTeach for a while where it originally started in Montreal. The organizers expanded last year to include Vancouver and now Toronto. I've never been there so it should be exciting (bonus: they have baseball).

If you're not familiar with DevTeach it's a rather intimate .NET/SQL developer conference that's pack full of world-renowned speakers. It's a lot of fun and packed with a lot of great content too. If you're not convinced, here's some more reasons to go.

Check out the sessions.

Register here.

Hope to see you there!

Postedby Beth Massi | 4 Comments    
Filed under: ,
Querying HTML with LINQ to XML
25 April 08 04:00 PM

Often times we need to parse HTML for data. Sure in a perfect world everything would have a nice service or API wrapped around it but as we all know this is not always the case. Many times we're left with parsing files or "screen scraping" to get the data we need from other applications. Sure this is brittle, but sometimes it's the best we can do. And sometimes you're just trying to get the data once so "good enough" is really good enough.

I was faced with that challenge myself this week. Yes even here not all systems expose services or if they do, finding the documentation or person to consult would take longer than writing a simple program. ;-) At the core all I needed to do was query a couple pieces of data from a bunch of web pages. This seemed like the perfect opportunity to use LINQ to XML because the structure of the page was pretty well formed HTML. However there were a couple tricks to figure out mainly because LINQ to XML doesn't support HTML entities. It only supports character entities and the built in XML entities (&lt; &gt; &quot; &amp; &apos;).

Working with simple HTML in an XElement is very straightforward, as long as it's well-formed and doesn't contain any HTML entity references:

Dim html = <html>
               <head>
                   <title>
                        Test Page
                    </title>
               </head>
               <body>
                    <a id="link1" href="http://mydownloads1.com">This is a link 1</a>
                    <a id="link2" href="http://mydownloads2.com">This is a link 2</a>
                    <a id="link3" href="http://mydownloads3.com">This is a link 3</a>
                    <a id="link4" href="http://mydownloads4.com">This is a link 4</a>
               </body>
           </html>


Dim links = From link In html...<a>

For Each link In links
    Console.WriteLine(link.@href)
Next

But as we all know HTML almost always contains entity references all over the place (like &nbsp; for the HTML space).  Also if you end up with any querystring parameters in your hrefs, when you try to load the HTML into the XElement, you get the same problem. Additionally if you paste a literal into the VB editor it places a semicolon into the querystring because it automatically tries to interpret it as an entity and places a semicolon where you don't want it.

So to fix this you need to remove all the unsupported HTML entity references as well as replace the & characters with &amp;. So in the pages I was loading luckily they were not that complicated and only contained &nbsp; and the problematic querystrings. This is an example of the page I was trying to load:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>
      Sample Page
    </title>
    <link href="css/page.css" rel="StyleSheet"/>
   </head>
  <body >
     <!--begin form -->
    <form name="form1" method="post" action="page.aspx?Product=Cool&amp;Id=12345" id="form1">
  
      <!--begin main table -->
      <table class="tblMain" cellspacing="0" cellpadding="0">
    
        <!--Properties -->
        <tr>
          <td class="tdHead">Properties</td>
        </tr>

        <tr>
          <td class="tdGrid">
            <div>
              <table class="grid" cellspacing="0" cellpadding="3" 
                     border="1" id="dgPage" style="border-collapse:collapse;">
                <tr class="grid_row">
                  <td class="grid_item" style="font-weight:bold;width:100px;">ID</td>
                  <td class="grid_item" style="width:480px;">12345</td>
                </tr>
                <tr class="grid_row">
                  <td class="grid_item" style="font-weight:bold;width:100px;">Published</td>
                  <td class="grid_item" style="width:480px;">05/04/2007</td>
                </tr>
              </table>
            </div>
          </td>
        </tr>

        <!--Details -->
        <tr>
          <td id="tdHeadDetails" class="tdHead">Statistics</td>
        </tr>

        <tr>
          <td class="tdGrid">
            <div>
              <table class="grid" cellspacing="0" cellpadding="3" rules="all" border="1" 
                     id="dgDetails" style="border-collapse:collapse;">
                <tr class="grid_header">
                  <th scope="col">Rating&nbsp;:</th>
                  <th scope="col">Raters&nbsp;:</th>
                  <th scope="col">Pageviews&nbsp;:</th>
                  <th scope="col">Printed&nbsp;:</th>
                  <th scope="col">Saved&nbsp;:</th>
                  <th scope="col">Emailed&nbsp;:</th>
                  <th scope="col">Linked&nbsp;:</th>
                  <th scope="col"></th>
                </tr>
                <tr class="grid_row">
                  <td class="grid_item" style="width:60px;">5.00</td>
                  <td class="grid_item" style="width:60px;">100</td>
                  <td class="grid_item" style="width:80px;">1000000</td>
                  <td class="grid_item" style="width:60px;">150</td>
                  <td class="grid_item" style="width:60px;">1000</td>
                  <td class="grid_item" style="width:60px;">100</td>
                  <td class="grid_item" style="width:280px;">40</td>
                  <td class="grid_item">
                    <a href="http://www.somewhere.com/default.aspx?ID=12345&Name=Beth" target="_blank">View</a>
                  </td>
                </tr>
              </table>
            </div>
          </td>
        </tr>
      </table>
     </form>
  </body>
</html>

So here's what I did to load this programmatically and fix up the HTML. Also notice that I need to add an Imports statement in order to import the default xml namespace that is declared in the HTML document otherwise our query later will not return any results.

Imports <xmlns="http://www.w3.org/1999/xhtml">
Imports System.Net
Imports System.IO

Public Class SimpleScreenScrape

    Function GetHtmlPage(ByVal strURL As String) As String
        Try

            Dim strResult As String
            Dim objResponse As WebResponse
            Dim objRequest As WebRequest = HttpWebRequest.Create(strURL)
            objRequest.UseDefaultCredentials = True

            objResponse = objRequest.GetResponse()
            Using sr As New StreamReader(objResponse.GetResponseStream())
                strResult = sr.ReadToEnd()
                sr.Close()
            End Using

            'Replace HTML entity references so that we can load into XElement
            strResult = Replace(strResult, "&nbsp;", "")
            strResult = Replace(strResult, "&", "&amp;")

            Return strResult

        Catch ex As Exception
            Return ""
        End Try
    End Function

    Sub QueryData()
        Dim html As XElement
        Try
            Dim p = GetHtmlPage("http://www.somewhere.com/default.aspx")

            Using sr As New StringReader(p)
                html = XElement.Load(sr)
            End Using

        Catch ex As Exception
            MsgBox("Page could not be loaded.")
            Exit Sub
        End Try
.
. 'Now we can write the queries...
.

Now for the fun part, the actual querying! Now that the document is loaded into the XElement the querying of it becomes a snap. I needed to grab the publish date, and then all the statistics from the page. This is easily done with a couple LINQ to XML queries, one query for each of the HTML tables where the data is located:

'I'm using FirstOrDefault here because I know my page 
' only has one of these tables
Dim stats = (From stat In html...<table> _
            Where stat.@id = "dgDetails" _
            Select fields = stat.<tr>.<th>, values = stat.<tr>.<td>).FirstOrDefault()

'Same here. FirstOrDefault because there's only one "Published" 
' html row (<tr>) on the page that I'm looking for.
Dim lastPublished = (From prop In html...<tr> _
                    Where prop.<td>.Value = "Published" _
                    Select prop.<td>(1).Value).FirstOrDefault()

Console.WriteLine(lastPublished)

For i = 0 To stats.fields.Count - 1
    Console.WriteLine(stats.fields(i).Value & " = " & stats.values(i).Value)
Next

And that's it. For this simple utility this is good enough for me and took me about 15 minutes to program using LINQ. The trick to loading the HTML document into an XElement is to remove all the unsupported HTML entity references first.

Enjoy!

Postedby Beth Massi | 9 Comments    
MVPs, VB and Me
21 April 08 07:15 PM

The MVP summit was last week -- my first summit where I wasn't an MVP but instead an employee -- and what an enjoyable experience it was! It was awesome seeing MVPs from all over the world. Microsoft really throws a great summit, this year renting out the entire EMP like they did a couple years ago. The difference this year was live band karaoke and Rock Band on the XBox set up on a real stage. Remind me to pick songs I know the words to next time I get up and sing!

One of the highlights of the summit was Adam Cogan, a Visual Studio Team System MVP from Australia, who brought over some beer for me on a request from Chuck Sterling. (Chuck realized I loved beer after dinner at his place with Sara Ford.) I figured since Adam was bringing over some Australian beer he could also bring me a Victoria Bitter.... not a very good beer but I LOVE the logo. Chris Williams, Visual Basic MVP, took a picture of me holding it up in one of the VB sessions:

If you can't see it, here's a picture of the bottle:

 

But the beer they really wanted me to try is called Little Creatures and I highly recommend it to the hop heads out there! Unfortunately one of the bottles broke in Adam's luggage (alcohol abuse!) but he saved one. Thanks so much for the trouble Adam, REALLY! Anyone that goes that that great length to bring a girl beer around the world is #1 in my book!

Enjoy! (you know I did ;-))

Postedby Beth Massi | 3 Comments    
Filed under: ,
LINQ to SQL N-Tier Smart Client - Part 3 Database Transactions
16 April 08 12:03 PM

In my previous posts this week I showed how to build a simple distributed application with a Windows client, a WCF hosted middle-tier and a data access layer that used LINQ to SQL:

LINQ to SQL N-Tier Smart Client - Part 1 Building the Middle-Tier

LINQ to SQL N-Tier Smart Client - Part 2 Building the Client

After sleeping on the design I realized that there's a scenario that we may want to handle. When we built the connected client-server version of the application (using the connected DataContext), because the DataContext is tracking all our changes (updates/inserts/and deletes) when we call SubmitChanges these updates are all processed in one single database transaction.

This may or may not be required for your application and in the case of Orders/OrderDetails it's okay to allow the updates and inserts and then the deletes to be processed in separate transactions. However what if we were working with drug interactions in a medical application or other data that needs to provide this level of integrity?

It's easy to make these modifications to our n-tier application we built. All we need to do is attach ALL the changes that we want processed in a single database transaction to one instance of the DataContext. To do this first we need to modify our service to accept all our changes. This can end up putting more data on the wire which we discussed in Part 1 so you need to evaluate your scenarios carefully. In our case I'm only pulling up open orders for a particular customer ID so the data set is relatively small.

First add the following interface on our WCF service:

<ServiceContract()> _
Public Interface IOMSService
.
.
<OperationContract()> _ Function SaveAllOrders(ByRef orders As OrderList, _ ByVal deletedOrders As IEnumerable(Of Order), _ ByVal deletedDetails As IEnumerable(Of OrderDetail)) As Boolean
End Interface

Next add the implementation to the OMSDataManager class in the data access layer to go ahead and attach all the changes to a single DataContext and submit all the changes at once. Note that the validation is performed exactly as before (when SubmitChanges is called).

Public Shared Function SaveAllOrders(ByRef orders As IEnumerable(Of Order), _
                                     ByVal deletedOrders As IEnumerable(Of Order), _
                                     ByVal deletedDetails As IEnumerable(Of OrderDetail)) As Boolean

    Dim hasOrders = (orders IsNot Nothing AndAlso orders.Count > 0)
    Dim hasDeletedOrders = (deletedOrders IsNot Nothing AndAlso deletedOrders.Count > 0)
    Dim hasDeletedDetails = (deletedDetails IsNot Nothing AndAlso deletedDetails.Count > 0)

    If (Not hasOrders) AndAlso (Not hasDeletedOrders) AndAlso (Not hasDeletedDetails) Then
        Return False 'nothing at all to save
    End If

    Dim db As New OMSDataContext

    For Each o In orders
        'Insert/update orders and details
        If o.OrderID = 0 Then
            db.Orders.InsertOnSubmit(o)
        Else
            db.Orders.Attach(o, o.IsDirty)
        End If

        For Each d In o.OrderDetails
            If d.IsDirty Then
                If d.OrderDetailID = 0 Then
                    db.OrderDetails.InsertOnSubmit(d)
                Else
                    db.OrderDetails.Attach(d, True)
                End If
            End If
        Next
    Next

    If hasDeletedOrders Then
        'Delete orders and related details
        db.Orders.AttachAll(deletedOrders, False)
        db.Orders.DeleteAllOnSubmit(deletedOrders)



For Each o In deletedOrders For Each detail In o.OrderDetails db.OrderDetails.DeleteOnSubmit(detail) Next Next End If If hasDeletedDetails Then 'Now delete the order details that were passed in ' (these order parents were not deleted, just the details) db.OrderDetails.AttachAll(deletedDetails, False) db.OrderDetails.DeleteAllOnSubmit(deletedDetails) End If Try 'There's one database transaction for all records that are attached. 'Since we attached all updates/inserts/deletes ' they will all be processed in one transaction. db.SubmitChanges(ConflictMode.ContinueOnConflict) 'Reset the IsDirty flag For Each o In orders o.IsDirty = False For Each d In o.OrderDetails d.IsDirty = False Next Next Catch ex As ChangeConflictException 'TODO: Conflict Handling Throw Return False End Try Return True End Function

We can then modify our form to call this new operation. On the client form I just added a new method called SaveAll. Note that the same simple change tracking is being used.

Private Sub SaveAll()
    'Push any pending edits on the BindingSources to the BindingList
    Me.Validate()
    Me.OrderBindingSource.EndEdit()
    Me.OrderDetailsBindingSource.EndEdit()
    Dim saved = False

    'Only save changes if there are some and they are valid
    If Me.HasChanges AndAlso Me.ValidateOrders() Then

        Dim saveOrders = Me.Orders.ToArray()
        Dim delOrders = Me.DeletedOrders.ToArray()
        Dim delDetails = Me.DeletedDetails.ToArray()

        Try
            If saveOrders.Count > 0 OrElse delOrders.Count > 0 OrElse delDetails.Count > 0 Then
                'Update/insert orders/details
                If proxy.SaveAllOrders(saveOrders, delOrders, delDetails) Then
                    Me.DeletedDetails.Clear()
                    Me.DeletedOrders.Clear()
                    saved = True
                End If
            End If

        Catch ex As Exception
            MsgBox(ex.ToString)
        End Try

        'Merges added keys and any validation errors
        Me.MergeOrdersList(saveOrders)

    End If

    If Me.HasErrors Then
        'Display any errors if there are any
        Me.DisplayErrors()
        MsgBox("Please correct the errors on this form.")
    Else
        If saved Then
            MsgBox("Your data was saved.")
        Else
            MsgBox("Your data was not saved.")
        End If
    End If
End Sub

So now when we make updates, inserts and deletes to our Orders and OrderDetails then we can save them all in a single database transaction.

I've uploaded the latest version of the application onto Code Gallery with the modifications.

Enjoy!

LINQ to SQL N-Tier Smart Client - Part 2 Building the Client
14 April 08 11:03 AM

In my last post we built the service and data access layer for our LINQ to SQL N-Tier application. In this post we'll walk through building a very simple Windows client form that works with our middle-tier.

Adding the Service Reference

Now that we have our middle-tier built it's time to add the service reference to the client project. Sine we have both .NET on the server and the client I'm going to use type sharing so that we can reuse the business objects (LINQ to SQL classes) on both ends. If you recall we we already added a project reference on the client to the OMSDataLayer project that defines these types.

Once you add that project reference we can add the service reference by right-clicking on the client and selecting "Add Service Reference" which opens up the Visual Studio 2008 Add Service Reference dialog. Hit the Discover button and it will pick up the OMSService in our solution. Click on the "Advanced" button and you'll notice some interesting settings here that I should mention.

Note here that the default is to "Reuse types in all referenced assemblies". This means that since we added the project reference to our LINQ to SQL business objects first, when the service proxy is generated it will not create new classes on the client, instead it will reference our business object types directly. Although this can make versioning more of a challenge it drastically cuts down the amount of code we have to write to maintain our business rules because now they are shared. However note that rules we call from the client cannot access the database directly. Our application here does not have any rules like that but it's something you may need to code for in your scenarios.

The other interesting settings I'll mention are the Collection type and Dictionary collection type settings since we're passing these types from our service. You can set these types to serialize differently if you need to. For instance, you can set the collection type to a BindingList if you are going to use all the collections from this service in typical data binding scenarios. Since this setting is for the entire service and we're only going to need a BindingList for just our GetOrdersByCustomerID result, I'm opting to keep the default Array type instead.

Loading the Data

Now we're ready to build our n-tier master-detail (one-to-many) form. Create a new form and then add a new data source (Menu, Data --> Add New Data Source) and select Object. Then expand the OMSDataLayer and choose the Order object and then do it again for Product.

Now we can build the master-detail form like I showed in this post (see the "Data Sources and Data Binding the Form" section) but this time against the objects in the shared assembly. The other main difference is that we don't need the Customer object because we're going to limit our data to just one customer.

Now we're ready to create an instance of our service reference and load the Orders from the middle-tier. Since the list will deserialize as an array, I'm going to place them into a BindingList that the form will manage. This will give us automatic add/delete support to the collection and a better data binding experience. I'm also going to set up a couple lists to track deletes of Order and OrderDetails. In a real application typically you create your own subclass of the BindingList and have it track these things but I'm trying to keep this example simple. We'll also load the products just like we did before but this time in our query we call the service instead.

Public Class NtierMasterDetailForm

    Dim customerID As Integer = 1 'should come from a search form

    Dim proxy As New OMSServiceReference.OMSServiceClient

    Dim Orders As New BindingList(Of Order)
    Dim DeletedOrders As New List(Of Order)
    Dim DeletedDetails As New List(Of OrderDetail)

    Private Sub Form1_Load() Handles MyBase.Load

        'Load the orders from our service
        Dim orderList = proxy.GetOrdersByCustomerID(customerID)

        For Each o In orderList
            Me.Orders.Add(o)
        Next

        Me.OrderBindingSource.DataSource = Me.Orders

        Dim emptyProduct As Product() = _
                {New Product With {.Name = "<Select a product>", .ProductID = 0}}

        Me.ProductBindingSource.DataSource = (From Empty In emptyProduct).Union( _
                                              From Product In proxy.GetProductList _
                                              Order By Product.Name)
    End Sub

Tracking Changes on the Objects

Now let's see how we're going to track all the changes made to the Orders and OrderDetails. First let's take another look at our BaseBusiness class. This is the class that we created in this post when we implemented our validation. When we built the middle-tier I mentioned that we needed to add this property but it's the client that needs to set it. Here's a look at the modifications we need to make to the BaseBusiness object including adding the DataMember attribute to the new IsDirty property as well as on the ValidationErrors dictionary.

<DataContract()> _
Public Class BaseBusiness
    Implements IDataErrorInfo

    Private m_isDirty As Boolean
    <DataMember()> _
    Public Property IsDirty() As Boolean
        Get
            Return m_isDirty
        End Get
        Set(ByVal value As Boolean)
            m_isDirty = value
        End Set
    End Property

    'This dictionary contains a list of our validation errors for each field
    Private m_validationErrors As New Dictionary(Of String, String)

    <DataMember()> _
    Public Property ValidationErrors() As Dictionary(Of String, String)
        Get
            Return m_validationErrors
        End Get
        Set(ByVal value As Dictionary(Of String, String))
            m_validationErrors = value
        End Set
    End Property
.
.
.

Since LINQ to SQL classes implement IPropertyNotifyChanged we can handle this event to set the IsDirty flag. The easiest way to set this flag is to tell the business objects themselves to do it. In order to hook up this event handler again when the objects are deserialized from the WCF service we can attribute a method with the OnDeserializedAttribute and add an event handler to the PropertyChanged event on all our business objects.

Partial Class Order
    Inherits BaseBusiness

    <OnDeserialized()> _
    Private Sub OnDeserialized(ByVal context As StreamingContext)
        AddHandler Me.PropertyChanged, AddressOf MyPropertyChanged
    End Sub

    Private Sub MyPropertyChanged(ByVal sender As Object, 
ByVal e As System.ComponentModel.PropertyChangedEventArgs) _
Handles Me.PropertyChanged If e.PropertyName <> "Customer" Then Me.IsDirty = True End If End Sub
.
.
.
 

The trick in the handler is to set the IsDirty flag only if the entity reference (the parent reference) property is not being set because we want to only set this flag if the user is making changes, not when the collection reference is set by the system.

Tracking adds is really easy because when an object is added to the collection it will be sent to the middle-tier and we can use the primary keys to determine if the Order or OrderDetail is new. For instance, if the OrderID on the Order is equal to zero (OrderID = 0) then we know we have a new object in the collection.

Deletes are a bit trickier because when you delete an object from the collection it's gone. If you are implementing a custom BindingList then you can just override the RemoveItem method but in our simple form we're just going to add the Order or OrderDetail being deleted to our Deleted* lists when the delete buttons are clicked on the form.

Private Sub OrderNavigatorDeleteItem_Click() Handles BindingNavigatorDeleteItem.Click
    'Track deletes of orders
    If Me.OrderBindingSource.Position > -1 Then
        Dim order As Order = CType(Me.OrderBindingSource.Current, Order)
        If order.OrderID > 0 Then
            'Greater than 0 indicates that the object came from the database.
            'If it's = 0 then we know the object was added here then deleted 
            '  and we don't need to track that.
            Me.DeletedOrders.Add(order)
        End If
    End If
End Sub

Private Sub DetailNavigatorDeleteItem_Click() Handles DetailNavigatorDeleteItem.Click
    'Track deletes of details
    If Me.OrderDetailsBindingSource.Position > -1 Then
        Dim detail As OrderDetail = CType(Me.OrderDetailsBindingSource.Current, OrderDetail)
        If detail.OrderDetailID > 0 Then
            Me.DeletedDetails.Add(detail)
        End If
    End If
End Sub

Validating and Saving our Changes

Before we send the changes to the service on the middle-tier we should validate the business objects here to save a round-trip. When we were working with the LINQ to SQL DataContext in connected mode the objects were validated when we called SubmitChanges(). This still happens in our middle-tier code but we need to validate here on the client as well so I added a public Validate method to the LINQ to SQL partial classes that just simply call into the OnValidate private methods we wrote previously. In the case of Order we'll also validate any OrderDetails.

Partial Class Order
    Inherits BaseBusiness
.
.
.
Public Sub Validate() Me.OnValidate(System.Data.Linq.ChangeAction.None) 'Validate the OrderDetails if there are any For Each d In Me.OrderDetails d.Validate() Next End Sub
.
.
.

Now we're ready to write our save code. If everything validates here on the client we first then send the deletes to the middle-tier, and if all goes well there then we clear the lists where we were tracking those objects. Then we can send the added and updated rows into the middle-tier. The middle-tier will then perform the validation there and then update and insert the business objects, and return the added primary/foreign keys. If we had any additional middle-tier business rules then those would also run and we could add additional validation messages that would be sent back in the ValidationErrors collection on each object.

The last thing left to do is dump the collection coming back from the middle-tier with our added keys back into the BindingList on our form. We just need to suspend the data binding first then we can copy the array back into the BindingList collection. Here's all the save code and supporting form methods.

Private Sub OrderBindingNavigatorSaveItem_Click() _
    Handles OrderBindingNavigatorSaveItem.Click

    Me.Save()
End Sub

''' <summary>
''' Saves all changes to the middle-tier
''' </summary>
''' <remarks></remarks>
Private Sub Save()
    'Push any pending edits on the BindingSources to the BindingList
    Me.Validate()
    Me.OrderBindingSource.EndEdit()
    Me.OrderDetailsBindingSource.EndEdit()

    Dim saved = True

    'Only save changes if there are some and they are valid
    If Me.HasChanges AndAlso Me.ValidateOrders() Then

        Dim saveOrders = Me.Orders.ToArray
Try If Me.DeletedDetails.Count > 0 OrElse Me.DeletedOrders.Count > 0 Then 'Delete any orders/details If proxy.DeleteOrders(Me.DeletedOrders.ToArray, _ Me.DeletedDetails.ToArray) Then Me.DeletedDetails.Clear() Me.DeletedOrders.Clear() Else saved = False End If End If If saved Then If saveOrders.Length > 0 Then 'Update/insert orders/details saved = proxy.SaveOrders(saveOrders) End If End If Catch ex As Exception MsgBox(ex.ToString) End