OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


LightSwitch Book Inventory Tracker Part 1

image

I’m using LightSwitch as a prototyping tool to get a jump start on a project at work that I took over from a long-exited programmer. The design I inherited has a lot of gaps that need to be filled in. I think the quickest way I can display those to my boss is to create the tables and basic maintenance screens so I can easily point out what’s there and what’s missing. LightSwitch inherently expresses database relationships in the screens it creates, so even if you aren’t going to use LightSwitch in the final application, you can quickly throw together and easily modify the table layout as you create the basic application in LightSwitch. Then you can step through the resulting screens and clearly see the table relationships with errors in design jumping right out at you.

The Requirements

The client has a need to track books as they move between various locations.

  • Locations contain multiple Buildings
  • Buildings contain multiple Rooms
  • Books are always assigned to one of:
    • A particular Room
    • OpenInventory, which has no specified location
  • Books can be transferred:
    • From Room to Room across all Buildings at all Locations
    • From any Room to OpenInventory
    • To OpenInventory from any Room
  • An AssignedInventory table row records the quantity and is uniquely identified by
    • Book
    • Room
    • Condition
  • An OpenInventory table row records quantity and is uniquely identified by
    • Book
    • Condition
  • OpenInventoryTransfers and AssignedInventoryTransfers include
    • Book
    • Condition
    • Quantity Sent
    • Received
    • Quantity Received New
    • Quantity Received Used
    • Condition Variance
    • Quantity Variance
    • Source (Open Inventory or Room)
    • Destination (Open Inventory or Room)

The nice thing about LightSwitch is that I can define classes, primarily in the form of table layout, that simultaneously define the underlying tables and the entities that represent them.  For instance, a Location class instance will have a Buildings collection property because I defined the one-to-many relationship between Location and Building. The underlying database will contain a Buildings table where each row holds a foreign key on a Locations table row. But I only see that represented logically, as a collection property in the Location class. And because I defined that relationship, LightSwitch can automatically generate a screen to maintain Location and Building records in a master/detail screen. I don’t have to create GridViews or the ListBoxes that bind to Linq queries written by me, and I don’t have to do it over and over again.

First, create the project:

image

In Solution Explorer, you can right-click / Open Properties, or double-click

image

Under Application Type select the Web Radio Button:

image

Now we start adding tables:

image

Instead of creating all the tables and then the screens, I’m going to create one table at a time, working from the bottom up, and create the requisite screens at that point. This will probably mean deleting and re-creating some screens as the design plays out, but it’s so easy it doesn’t really matter. And I could use the screen designer to make the changes.

Let’s begin with the Book table. Create the columns as shown and make ISBN a unique index:

image

You might want to have a separate Authors table, but the design I was given specifies Author name in the Book table.

We want to validate ISBN:

image

Eventually we’ll want to allow for hyphens and validate that the ISBN is well-formed,  but for now we’ll just check for a length of 10 or 13 and all numeric digits:

partial void ISBN_Validate(EntityValidationResultsBuilder results)
{
    if (this.ISBN.Length != 10 && this.ISBN.Length != 13)
        results.AddPropertyError("ISBN must be either 10 or 13 digits long");
    else
    {
        foreach (char ch in this.ISBN)
            if (ch < '0' || ch > '9')
            {
                results.AddPropertyError("ISBN must be all numeric");
                break;
            }
    }
}

Probably I could have checked for a Long Positive Integer, but In general I only like to treat numeric strings as numbers when they’re actually used as numbers. Anyway, this code will ultimately have to validate an actual ISBN.

Now we’ll generate a screen so we can do CRUD operations on Books.

image

The Editable Grid Screen (1) is what we want. By selecting Books (2) in the Screen Data dropdown we get the Screen Name (3) of EditableBooksGrid. Let’s stick with that, at least for now.

image

And that’s it. Hit Ctl-F5 and let’s see what we have.

image

We see the screen we just added in the menu (1) and it’s the default tab in a tabbed view (2) that we can page through (3). We have controls to do CRUD operations (4) and to save and load the Books table (5). As a bonus, if we’re running in debug mode we can change the screen design at runtime (6).

We’re about to add some definitions of Books, by creating instances of the Books entity, which will be stored in rows in the Books table, but first let’s adjust the column layout a bit. Even though ISBN is the unique key, I’d prefer to see Title in the first column. That’s pretty easy to do. Select the EditableBooksGrid screen in Solution Explorer, then just drag Title so it’s above ISBN:

image

By selecting the green plus sign Icon we can enter the Book into an individual form. Below I’ve done that. As soon as I enter a value in ISBN and tab away validation finds and displays my error:

image

So I’ll correct the length, but leave in an alpha character. Below I’ve clicked on the field so you can see how the message is displayed in that case.

image 

I’ll enter a ‘valid’ ISBN and leave the required Title and  Name fields blank then click OK. One quirk with this validation is that you can tab away from an empty required field without triggering the error, if it starts out empty. That’s because binding is only triggered when the value changes, and the validation occurs as a result of binding. Leaving an input field empty does not change the value, so these errors of omission are not caught until you click OK and trigger overall form validation.

image

To show you the difference, I cleared out the first record entirely by clicking on the ‘X’ icon, entered a character in the Title field, tabbed to and entered ISBN, then tabbed back to Title cleared it and tabbed away. I got this message immediately. Notice the error message wording is different, which implies that the business rule is being defined twice in the underlying code. Fortunately LightSwitch handles that, so I don’t have to remember to maintain it in two places:

image

Now I’ll enter duplicate ISBN numbers. When I click on the Save icon a modal generic error message box appears (not shown) followed by:

image

One last thing, and then we’ll move on. This form is wasteful of screen space so let’s make some adjustments, but let’s do it at run-time by clicking F5 to run in debug mode, then clicking on the  Design Screen icon in the upper right corner of the screen. Select ISBN and set the Width to 90 pixels and click Save. You can try MaxWidth, but I ended up with padding on the left when I did, and specifying the number of characters doesn’t work any better than it does in HTML:

image

Do the equivalent for the Name fields and it looks a lot more reasonable:

image

I entered Mark Twain’s first and last name in the wrong order which gives me an opportunity to show you what you get if you select ‘Tom Sawyer’ and click on the Pencil icon:

image

I correct the name and press ok, but I still have to click on the Save icon to actually make the changes to the data. If I try to exit the screen without clicking on Save:

image

And now for something completely different. Not really, we’re just going to the other extreme in the sense that I said I was going to do the tables from the bottom up. But now I’m going to start at the top and define the Location table because Location is the other entity that has no dependencies on other entities / tables, which means I can create the Location screen as soon as I create the table. Then I’m going to create the Buildings table and show you something wonderful.

image

Actually, I think I’ll go ahead and create the Buildings table. Note the default naming convention in effect. I create a Building class (single tense) and instances of the class, or at least the elements of which must persist, are stored as rows in the Buildings table (multiple tense).

image

The address fields are optional for Building. It may be that we want to restrict access to the Building can only be reached through the Location’s address so it’s a business rule that the Building address can be null.

Now the magic starts to happen. I said up front that a Location may have many Buildings. We can set that up with just a few mouse-clicks:

image

Edit the Building class (1). Click on Relationship… (2). A dialog box comes up as shown above. The From table is Building (3) and it’s on the many side of a one-to-many relationship. In the graphic you can see the infinity sign next to the Building icon (4) and a ‘1’ (5) next to the icon representing the still-to-be-designated ‘Master’ table , which will be Locations. We could have started by editing the Location class, but then we would have to switched around the tables in the Add New Relationship dialog box.

As shown below, choose Location for the To table (1). The semantics are a little fuzzy in that the table names are actually Locations and Buildings. The right icon (2) is now labeled Location with the enclosed word Buildings representing the Buildings collection associated with the Location. In Sql Server parlance this would equate to the Building rows that hold foreign key pointers on the Location table. The ‘1’ indicates that Location is the one side of the one-to-many relationship.

image

After selecting Location, on the left side we have the Building icon (3) with the embedded word Location representing the one and only Location that the Building can be associated with. Again, this is the Location row that the Building row Location foreign key points to. The infinity sign indicates that Building is on the many side of the one-to-many relationship.

You can see the referential rules inferred from the relationship. (4).  Until you’re used to the interface it’s possible you’ll get the relationships backwards sometimes. Looking at these rules when you set up the relationship is your best chance of catching that early. In this case, if they said a Location must have a Building, I’d know I got it backwards

Actually I think if I’d paid closer attention to labels, as in Navigation Property (5) I wouldn’t have had my relationship problems. if you’re not used to the interface and are just zipping along doing your RAD thing it’s easy to take the Navigation Property value in the From column as the From table, when of course it’s just the opposite, so just be aware and maybe you won’t make the same mistake.

In general, if you are editing the class that represents the table on the many side, in this case Building,  when you click on Relationship… then the default settings in the Add New Relationship… dialog will be the ones you intend.

Now when we look at the Building class below we see more symbols to indicate the relationship between Location and Building. You can see the ‘1’ next to the Location icon and the infinity symbol next to the Location column / property in the Building table / class. This indicates there may be many Buildings, but because it’s next to the word Location it’s easy to read incorrectly at a glance. I thinks it’s just a matter of building the visual associations through repetition so it becomes second nature.

image

I used the terms ‘column / property’ and ‘table / class’ to emphasize that you don’t really have to worry about which is which. How great is it that you can build tables, add the relationships, and those relationships are automatically expressed in the  generated classes that model that data?

So here’s the cool part, we’re going to create a screen that lets us maintain both Locations and Buildings. This is the second place you might notice that you got the relationship backwards. If you don’t see a Location Buildings checkbox then you forgot to set up the relationship or you got it backwards:

image

Select List and Details Screen and the Locations table in the Screen Data dropdown. Uncheck Location Details and make sure Location Buildings is checked. Select ‘Ok’ and click ctrl-F5 to run the application:

image

We select the Locations List Detail entry in the menu (1) which brings up the tabbed page (2). The Locations list box (3) will show all the Locations. The Buildings list box (4) will show the collection of Buildings associated with the currently selected Location.

Click on the green plus sign icon under Locations to add one:

image

Now here’s a test for LightSwitch. We haven’t actually added a Location record yet because we haven’t clicked Save. Will LightSwitch allow us to add a Building to the still to be created Museum of Science Fiction Location?

image

Here we go:

image

So far, so good. All that remains is to click on the Save icon, and when I do, it works.

We haven’t done any validation of the input. What do we need? In the case of Location, I have a general rule I’m making up right now is that every entity needs a unique identifier that can be displayed. Location does not exist within the context of any other entity so for Location the Name property will be unique:

image

We want Building Name to be unique within Location. LightSwitch is up to it:

image

image

LightSwitch adds a compound unique index that includes Name and Location.

image

I can use the same Name at a different Location however:

image

Now for the Room table:

image

For now it just has a Name column / property and a many-to-one relationship to Building. Now let’s set up an Editable Grid Screen because I want to make a point about what you will see in the Buildings dropdown.

image

Not too useful. Remember I said that every entity needs a unique, displayable property. This is why. Fortunately this can be easily done. Edit the Building class and click on <Add Property>:

image

Name it DisplayName and click on the Is Computed checkbox which will result in:

image

Click on Edit Method:

namespace LightSwitchApplication
{
    public partial class Building
    {
        partial void DisplayName_Compute(ref string result)
        {
            result = this.Name + " / " + this.Location.Name;
        }
    }
}

Edit the Building class. Click on Building in the Header. Under Properties set Summary Property to DisplayName:

image

And lo:

image

One last thing we need to do is make Room Name unique within Building. You can do that by setting the Include in Unique Index  property on for Name and Building in the Room class as we did for Name and Location in the Building class.

Let’s add a Room

image

Now it’s time to create some inventory.We’ll start with OpenInventory, each instance of which represents some quantity of a particular Book that is not currently assigned to any Location:

image

Create an OpenInventory class as shown and click on Choice List under Condition Properties; Create the New and Used values that can be assigned to the Condition property. One of the things that differentiates instances of OpenInventory and AssignedInventory  is the Condition of the Books; all the Books within a particular OpenInventory or AssignedInventory instance  are in the same Condition, either New or Used.

image

Now we establish the relationship between OpenInventory and Book. An OpenInventory can only be linked to one Book, but a Book can be linked to many OpenInventory rows. Actually only two, New or Used, which is just a matter of setting another unique index.  By virtue of the choices I defined above, the property will always be restricted to New or Used. If it’s also unique there can only be two instances.

But that unique index includes both Book and Condition, so let’s add the Book Relationship. OpenInventory is on the many side, so we click on Relationships while editing OpenInventory:

image

Now we can set the Include in Unique Index property on for Condition and Book.

image

You have to set them individually. The shot above is setting the property for Book.

Let’s go ahead and create the Editable Grid Screen for OpenInventory.

image

In this case we have a unique identifier but it’s not very helpful. We’ll need to add a computed property to Book.

partial void DisplayName_Compute(ref string result)
{
  result = this.Title + " (" + this.ISBN + ")";
}

Don’t forget, like I just did, that you have to set the Book class Summary Property value to DisplayName.

image

I haven’t quite figured out why the Listbox columns sometimes get re-sized automatically and other times not, but you can see that we now have a combination of Title and ISBN displayed for Book.

image

Let’s run in debug so we can change the width until we get it right. Hit F5 and click on Design Screen when it comes up:

image

After clicking Save:

image

Close enough. Afterwards I found it’s a little quirky about when it displays the drop down part full-width and when it clips it at the column width, but still close enough for now.

Let’s test whether the Condition / Book combination is a unique key on OpenInventory:

image

Sure enough, I cannot create two OpenInventory rows for the same Book and Condition.

I also want to change the column order in the form. It’s as simple as changing this:

image

to this by dragging and dropping:

image

I also widened the Book Listbox to 180 pixels.

image

The other columns are still excessively wide, but let’s move on to the AssignedInventory class, each instance of which is like OpenInventory in that it represents some quantity of a particular Book in a particular Condition but adds on the assignment to a particular Room in a particular Building in a particular Location. Accordingly we’re going to require a computed property to display for Room.

Bur first things first. Here’s the class before we add the relationship to Book. We have to create New and Used choices for Condition again. There may be a way to have AssignedInventory inherit from OpenInventory, layering on the Room property, but trying to do that and reflect the underlying tables at the same time is too much of a distraction. I would have a single BookInventory table with have a nullable Room link to represent both with a single table, but my existing design spec at work calls for separate tables.

image

AssignedInventory is on the many side of the Book relationship, so click on Relationship

image

AssignedInventory is also on the many side of the Room relationship so click on Relationship… again:

image

And that’s all we need to define AssignedInventory. Let’s see what an Editable Grid Screen looks like:

image

Not enough information. We don’t know what Building the Room is in. Time for another Summary Property.

partial void DisplayName_Compute(ref string result)
{
    result = this.Name + " / " + this.Building;
}

After also making some adjustments to column order and width:

image

Notice that I concatenated this.Building to this.Name in DisplayName_Compute; what I get from this.Building is the Summary Property of Building, which includes Location in the Name, so Room is truly uniquely identified in the ListBox.

The combination of Book and Condition also need to be unique, as in OpenInventory, but only within Room.  Therefore we set the Include in Unique Index for Book, Room, and Condition. Notice I’ve also dragged the column names into a more logical order:

image

image

Now for Book transfers. We’ll do OpenInventoryTransfers first.

image

I went ahead and defined the relationships and fields in a logical order. In this case the combination of Book and Condition is not unique, even within Room because you could have multiple transfers in process to or from  a single Room for the same Book and Condition. It’s after they are received that the quantity from each will be deducted from the source inventory and added to the destination inventory uniquely identified by the same Book and Condition within the destination.

Once again it’s necessary to build New and Used choices for Condition, and now we add DestinationType with the choices of OpenInventory and Room. An OpenInventoryTransfer can be in either direction, from OpenInventory to Room or vice-versa. DestinationType identifies which.

image

ConditionVariance and QuantityVariance will be two computed fields that will indicate a variance in what was received from what was sent. Also we require QuantitySent to be greater than zero:

partial void ConditionVariance_Compute(ref bool result)
{
    result =
        this.Received &&
        ((this.Condition == "New" && this.QuantityReceivedUsed > 0) ||
        (this.Condition == "Used" && this.QuantityReceivedUsed > 0));
}
partial void QuantityVariance_Compute(ref bool result)
{
    result =
        this.Received &&
        (this.QuantityReceivedUsed + this.QuantityReceivedNew != this.QuantitySent);
}
partial void QuantitySent_Validate(EntityValidationResultsBuilder results)
{
    if (this.QuantitySent < 1)
        results.AddPropertyError("QuantitySent must be greater than zero");
}

Let’s go ahead and create the Editable Grid Screen.  I’ll cut to the chase and show you the detail screen as the field sizes need serious pruning.

image

Notice that Condition Variance and Quantity Variance are displayed as labels rather than text boxes in the screen LightSwitch created by default:

image

That’s because we converted them to computed fields. If they were actual table columns they would be on the form as text boxes:

image

image

Now that Room is on the one side of some one-to-many relationships we should create a List and Details Screen:

image

We see the AssignedInventory collection for the selected Room

image

… and the Open Inventory Transfers Collection

image

We can create new Rooms, new Assigned Inventories or Open Inventory Transfers to and from the selected Room from this screen. And you always have the luxury of form based entry when row based entry is too cumbersome:

image

After clicking OK:

image

Onward to AssignedInventoryTransfer, which is a transfer between two rooms. As you’d expect each AssignedInventoryTransfer has a Source Room and a Destination Room, putting Room on the one side of two one-to-many relationships:

image

How does this play out in the Room List and Details Screen? We need to re-create it to include the new collections:

image

Hit F5 to run in debug mode and we’ll modify the displayed collection names using Design Screen:

image

We can add transfers from this screen:

image

But first let’s add a business rule that the Source and Destination Rooms cannot be the same:

image

partial void AssignedInventoryTransfers_Validate(AssignedInventoryTransfer entity, EntitySetValidationResultsBuilder results)
{
    if (entity.Source == entity.Destination)
        results.AddEntityError("Source and Destination must be different Rooms");
}

Now if we try to create a transfer that violates this rule:

image

We can get this far:

image

But when we click Save:

image

We can quickly fix the mistake:

image

I think we have a complete set of screens to support basic CRUD on all our tables. I want to do some renaming and pruning. Let’s review:

We have the Editable Books Grid which allows us to create and maintain Book table rows.  Book definitions stand on their own so we need a standalone form to edit them. Let’s change the screen title to Book Catalog.

image 

We can maintain both Locations and Building through Locations List Detail which we’ll rename Locations And Buildings.

image

Editable Rooms Grid allows us to maintain Rooms but we can do that in Rooms List Detail, so it’s out.

OpenInventory can only be maintained in Editable Open Inventories Grid so we keep it under the name OpenInventoryScreen, OpenInventory being taken.

image

Both Editable Assigned Inventories Grid and Editable Open Inventory Transfers Grid can go. Assigned Inventories and Inventory transfers are always linked to a Room and so can be maintained through Rooms List Detail which I’m renaming Rooms And Books Inventory.

image

All the essential pieces are in place. We can create Book Definitions, Book inventory and Rooms for Book inventory assignment. We can create transfers between OpenInventory and Room and between two Rooms. We can record receipt of those transfers and variances between what was sent and what was received.

This is getting kind of long, so I’m going to post this as Part 1. One glaring necessity is validation of quantities for transfers. Are enough copies available at the source? And what about transfers that haven’t been completed when determining how many copies of a particular Book are available? Also we need security and accountability in the form of users and roles.

The next day…

Another omission just jumped out at me. The OpenInventory entity needs to have an OpenInventoryTransfer collection property and AssignedInventory entity needs to have an AssignedInventoryTransfer collection property – meaning the OpenInventoryTransfers table needs to hold a foreign key on the OpenInventory table, etc. so stay tuned for Part 2.

Ouch, I also duplicated Book and Condition in OpenInventoryTransfer. I can only think that in my head I saw the combination as the foreign key that OpenInventoryTransfer holds on OpenInventory. My only defense is LightSwitch makes it so easy you don’t do some things you normally do without thinking about.





Comments are closed.
Showing 1 Comment
Avatar  Michael Washington 7 years ago

It's nice that you are covering an unique application with a lot of business logic. Shows the real power of LightSwitch. I know that it is not your decision to use LightSwitch for the actual application, however, if it was you could complete it in a day.