Table of Contents
- Excursus 1: master the ObjectBrowser and its Usage
- Explore the typed Dataset
- The planned Application-Enhancement
- Typed coding against typed Dataset
- Avoid untyped DataTypes
- Form-transcending Databinding
- Excursus 2: Input-Dialogs
- The Helpers-Project
- An Excercise left to you
This article presupposes two previous articles as known (or as in doubt to consult):
- Relational Datamodel for Beginners explains, what a relational Model is, and how to design it as typed Dataset
- Databinding for Beginners introduces "four Views" as general Data-Presentation-Patterns, and gives guidance to create such Views by binding Controls to typed Datasets
The current Article now covers four topics:
- learn to explore the typed Dataset with the ObjectBrowser
- use the explored things (the typed of the typed Dataset) to endow the sample-application with a more rich User-Experience
- solve the difficult problem of form-transcending Databinding
- create databound Input-Mask-Dialogs.
Excursus 1: master the ObjectBrowser and its Usage
The propably most under-used Visual-Studio-Tool ever is the ObjectBrowser. You find every single class documented there, with all its properties, methods, events, fields, and you can browse from type to type, as well as inspect base-classes and implemented Interfaces.
Fetch it here:
Note the shortcut and keep it in mind. I strongly recommend very frequently to open the Objectbrowser and browse the Code-Documentation - even when you think you already know what you think that you need to know.
Here a search for
Did you know
FileInfo, and if so, did you know, that it has five methods to open Files for read and write? And if so, did you know all the Exceptions, which can occur?
You can improve the search-result with navigating back and forward:
(The tooltipped shortcut is wrong, correct is: (Alt Left/Right))
Seems stupid to do so, but that dummi-navigation brings up the Placement of FileInfo in the Framework-Systematic:
Now on the left you see other, similar and related classes, eg. DirectoryInfo, DrifeInfo, Path, Stream, StreamReader, BinaryReader and stuff, which can be interesting. And of course you can browse around via Hyperlink.
Another way to consult that Documentation is directly from Code-Editor: Contextmenu on a ClassMember in Question, then "GoToDefinition"
(I recommend to keep the shortcut in Mind too)
Unfortunately that "Go To Definition" only works on VB.Net. In c# it leads to a more raw info-presentation, quite uncomfortable, compared with the ObjectBrowser. C#-Programmers must get by with the OB-Search as shown above.
(Tip to c#-Programmers: Open a Vb-Project, and use the Vb-ObjectBrowser (even for tryal). Its search-results often are more significant, for instance, when a class is searched, then not all its Constructors populate the result-view. Moreover the VB-OB's-Member-View displays Return-Types at a glance - the c#-OB does not so.)
I bother you once again with a FileInfo-Screenshot, now I have expanded the Base-Types of some classes, which brings up a glimpse to Kinship as well as to implemented Interfaces:
Very important are appropriate OB-Settings
I recommend "View Containers", "Show BaseTypes", "Show PublicMembers".
In cases "Show Inherited Members" is useful as well, but mostly it overcrowds the Display, and for that it is easier, if you want to see inherited Members, to navigate to the baseclass (as hinted in the FileInfo-screenshot above).
As "Browse"-Setting I recommend "My Solution" - but also note the many other available setting-options, especial "Custom Component Set" can be useful in cases.
An important setting is accessible as ContextMenu on the Member-View:
"Group by Member Type" gives option to collapse Groupes of no Interest, eg if you're especially looking for a particular Property or stuff.
Again I entrust to you: Use the ObjectBrowser frequently. If you didn't yet, use it right now, as excercise:
List(Of T) - Class. Explore its
FindLastIndex() - Functions (and Overloads) as well as its
InsertRange(), and of course its four Overloads of
And don't forget to check the implemented Interfaces, browse them, and figgure out, what they are for.
One more Tip: Press F1 in ObjectBrowser to open the the aimed Documentation on MSDN. For instance the
.BinarySearch() MSDN-Code-Sample might be instructive.
Explore these members: Select them, read the Summaries, Browse to Parameter-Types (if present), and learn how they work. Again - if you didn't yet, do it right now - I assure: That's no loss of Time.
Next find out, how the same stuff is achieved in the
Then switch to
DateTime, explore the 10(!) ways of Adding something to a
DateTime. And note the four ways to subtract from a Date (and note the differences). See Dates Parse/TryParse - Infrastructure (9 Members) and compare, how that is done on
Don't be one of the Programmers only knowing what Intellisense sais, and Google, and then ask in Forums! X|
Or plain spoken: You can't consider you as ".Net-Programmer" as long as you're such unfamiliar with such fundamental Datatypes (namly
Sorry for the long excursus, but I must ensure that the ObjectBrowser is well-known - otherwise we can't step forward to explore our typed Dataset at all.
Explore the typed Dataset
First recall the Datamodel, we developed in the previous Article:
It can be seen as Part of a mailing-Warehouse-DataModel, which represents, how customer can order articles: Each Order has many OrderEntries, an OrderEntry mainly sais, how many of which Article the Customer wants.
Now see our own Application in ObjectBrowser:
Thanks to our OB-Setting "View Containers" the own Application (as a Container) appears as Top Level-Node on the left. Look: my own Code is very few: I only created four different Forms, three of them intended to be used as Dialogs.
By contrast the Dataset-Designer created 21 Classes and five Delegates. The Systematic of its creation is as follows:
Firstly it creates the typed Dataset -
OrderDts - itself. As you see (bottom right), it inherits from (untyped)
All the other stuff is implemented as nested class - that's why there is not an
ArticleDataTable, but it is an
OrderDts.ArticleDataTable (which is a difference!)
Theese nested classes are the emanations in code, of what we designed in Designer as our Entities.
To each Entity is generated
- a typed DataTable-Class
- a typed DataRow-Class
- a typed ChangeEventArgs-Class
- a typed Eventhandler-Delegate
The latter two are neither that complicated nor important at the moment.
Firstly careful watch our
OrdersDts-Members above, especially the Properties: Each Table is directly accessible as typed Property of the OrdersDts, namely
Article, Category, Customer, Order, OrderEntry.
After that enter the
Re-consult the Order-Entity in Dataset-Designer, and you will recognize, that every Entity-Attribute has generated an associated DataColumn.
But much more important is, how to Add
OrderRows to an
OrderDataTable: There are 3 Overloads:
Sub AddOrderRow(row As OrderRow) (the first Member from Top on)
This one is quite uncomfortable, since it requires a new, ready and valid
OrderRow. You can create such by the Function
OrderDataTable.NewOrderRow(), then populate its Properties, but as said: a bit cumbersome and unsafe, compared with the other AddOrderRow-Overload. (but might be useful in cases.)
Unfortunately Intellisense always presents this member as first choice - but avoid it - and take the second choice:
Function AddOrderRow(parentCustomerRow As CustomerRow, OrderDate As Date, DeliveryDate As Date, ShipDate As Date, ShipCosts As Decimal) As OrderRow
Looks much more cumbersome, but the opposite is true: It is a handy form for you to fill in all what's needed to build a valid
Trust me: This approach is more convenient, and foremost it is more save, since you can't forget one of the Entity-Attributes to assign.
Note, that it adds the new OrderRow to the Table and also returns it to the caller. This is very convenient, since code often needs to work on with just added typed Datarows.
- The third
AddOrderRow()-Overload doesn't matter, it only appears, because I've added an calculated Expression to the Order-Entity - I'll come back to that later
Also note, that it is the DataTable, which provides powerful Events (the DataRow does not so). These Events (and some more, provided by the Base-Class) can become cruicial when you develop advanced Businesslogic, eg when you need to react before/after Changes/Deletions.
Now explore the particular
OrderRow, which will populate our
Here you see each Entity-Attribute emanated as typed Property:
Note their strong typing - the BaseClass -
DataRow - 1) only retrieves Values of Type
Object, and 2) is only accessible with the proper String-Key.
(Both together is the Main-Code-Smell of untyped DataRow - a third smell is 3) the risk, to mix up DataRows of different Entities)
Then especially note the
CustomerRow-Property: In Code you will never-ever need to access any ForeignKey - if you do so, you do something wrong. It's wrong, because it's always easier to use the ParentRow itself, instead of a Foreign-Key (wich at least is nothing but a stupid number).
Keep that in mind for further usage: Every ChildRow knows its ParentRow(s) (remind the relational Principle: an Entity can have several Parents)
Note with same care the
GetOrderEntryRows()-Function: Every ParentRow knows all its Childrows
Next note the cumbersome Impacts of nullable designed Attributes: As said you never can retrieve a value directly, when in Dataset-Designer
AllowDbNull=True is configured. You must always do the IsNull-check before, and that is what theese cumbersome
IsXyNull()-Functions are for.
(And now you also understand, what the
SetXyNull()-Methods are for.)
Note, that in contrary to the above the
OrderDate-Property is not accompagned with such inconvenient "hoo-ha"-stuff.
OrderDate.AllowNull=False, such circuitousness is dispensable.
Let me summarize, how a typed Dataset generally is composed - there is:
- one typedDataset, inherited from System.Data.Dataset
- to each Entity a particular typed DataTable, inherited from System.Data.DataTable
- to each Entity a particular typed DataRow, inherited from System.Data.DataRow
These generated classes and their members provide everything you need, to keep away from smelly type-casts, smelly string-Key-Accesses and all combinations of that.
The challenge is, them actually to use, and strictly to avoid there untyped base-classes - (I will rant about that later ;))
(Sidenote: You also can inspect the generated code directly: just open the OrderDts.Designer - File. But because of the huge quantity of code that brings no good overview)
We've already had a small "finger-gymnastics" about coding against typed Dataset in the first article. Now I come along with a more practical sample, which will implement some User-Experience-Enhancements to the Sample-Application:
Customer=>Order=>OrderEntry - (Parent-Child-Child-)View above now can add or edit Orders by a specialized Order-Edit-Dialog. Eg on DoubleClick in the MainForm the "Edit Order"-Dialog opens with the currently selected Order:
The Top of the "Edit Order"-Form is a DetailView of the in Mainform selected Order. Below there is a general ParentChildView
Category=>Article, as introduced in the previous Article. But here the ArticleGrid additional provides the blue "Ordered"-Column to the User, where he just can enter, how many Articles of a particular he wants.
And that is what he orders :-D
Please check it out and satisfy yourself, that it is even easyer than flipping through the pages of a real-world-warehouse-catalogue, pointing at things you want :-D
To support that I've added two Article-Columns in the Dataset-Designer, which are meant as temporary:
Before the OrderEdit-Dialog opens, the
TemporaryCount-Values are to populate from the current Orders OrderEntries, which might already exist.
For that they appear in the blue column as Dialog-Default-Values, which the user can change.
The other newly added Column,
Article.TempSumPrice is a calculated Column, which calculates the PriceSum as follows:
TemporaryCount * Price - I hopefully don't need to explain that.
Now the user can browse all Articles of all Categories, and to order an article, he just enters, how much he wants.
Most of all still does Databinding for us, but some logic is left, before opening and after closing the Order-Edit-Dialog:
- before Opening each OrderEntry of the selected Order must copy its
.Count-Value to its ParentArticles
- after Closing each OrderEntry either copies its ParentArticles .
TemporaryCount back to its
Or - if .
TemporaryCount was changed to
0 - delete the OrderEntry.
Additionally all Articles must be queried, whiches .
TemporaryCount was changed from
0 to a greater number - for theese Articles there are new OrderEntries to create.
(by the way - recognize CRUD: create, read, update, delete ;))
I must admire, achieving theese Goals is no longer really Programmer-Beginner-Stuff.
Hopefully you master the vb.net/c# - Language so far, that you understand Extension-Methods, anonymous Methods and some basics of Linq.
1 Private Sub LaunchOrderEditDialog()
2 Dim rwOrder As OrderRow = bsCustomerOrder.At(Of OrderRow)()
3 Dim orderEntries As OrderEntryRow() = rwOrder.GetOrderEntryRows
4 For Each rwEntry In orderEntries
5 rwEntry.ArticleRow.TemporaryCount = rwEntry.Count
7 If bsCustomerOrder.EditCurrent(Of dlgOrder)() <> DialogResult.OK Then Return
9 For Each rwEntry In orderEntries
10 Dim rwArt = rwEntry.ArticleRow
11 If rwArt.TemporaryCount > 0 Then
12 rwEntry.Count = rwArt.TemporaryCount
13 rwArt.TemporaryCount = 0
16 End If
18 For Each rwArt In OrderDts.Article.Where(Function(rw) rw.TemporaryCount > 0)
19 OrderDts.OrderEntry.AddOrderEntryRow(rwOrder, rwArt, rwArt.TemporaryCount)
20 rwArt.TemporaryCount = 0
22 End Sub
I will walkthrough line by line:
get the currently selected OrderRow from the proper BindingSource.
BindingSource.At(Of OrderRow)() is one of my Extensions to simplify BindingSource-Usage - quasi a shortcut for
DirectCast(DirectCast(bsCustomerOrder.Current, DataRowView).Row, OrderRow)
Sorry, the contained Data of a BindingSource is encapsulated within
DataRowViews for some reasons, and that makes accessing the contained typed DataRows that cumbersome.
3) Usage of the faboulous typed
GetXYChildRows()-Function, mentioned in the Explore-Dataset-Paragraph
4 - 6) Copy eaches OrderEntrys
.Count-Property to its Parent-Articles
(using the fact, that each OrderEntryRow knows its Article-Parent, as mentioned in Explore-Dataset)
7) A real advanced BindingSource-Extension launches the OrderInput-Dialog (I will come to that later).
LaunchOrderEditDialog() aborts, if the returned
DialogResult is not
10) Get eaches OrderEntries Article-Parent
11 - 16) Either update OrderEntries
.Count or delete it. Note the cleanup (line#13) - this is cruicial when later querying newly ordered Articles
18) Query all Articles with
.TemporyCount>0, if any are left. These Articles are newly ordered by the User.
19) Create new OrderEntries, using the faboulous
AddXyRow()-Function, mentioned in Explore-Dataset.
You see: Within 20 lines of code we solved a non-trivial Business-Logic-Problem.
Thereby we used 14 times (if I count right) a generated typed Dataset-Member.
All Accesses are strongly typed - neither type-casts nor string-key-smells were required.
Only the two accesses to the BindingSource must be seen as untyped, so that they needed additional Type-Information. (of course - since BindingSource is no Subject of DatasetDesigners typed Code-Generating)
A rant: Avoid untyped DataTypes
Avoid them like the plague (which it is, indeed X|)
Whenever anywhere in your code one of theese three words appear:
Dataset, DataTable, DataRow - you have done something completely wrong.
You have a typed Dataset, so use it - see the code above: It deals with three different DataTables, and five different DataRows, but nowhere appears a
OrderDts does the work, and its typed Tables:
OrderDts.Article As ArticleDataTable ,
OrderDts.OrderEntry As OrderEntryDataTable, etc..
Don't fall back into Codesmells like
Dim tbOrder = OrderDts.Tables("Order")
You will not get happy with that, because it has no safe and comfortable
AddOrderRow() As OrderRow-Function, and the Rows it retrieves also are untyped, means: have no typed Members like
rw.GetOrderEntryRows() As OrderEntryRow() or even
rw.OrderDate As Date.
One Step into theese smelly Code - and you stick in the mud, and you'll continue with stuff like
Dim orderEntries As DataRow() = rwOrder.GetChildRows("FK_Order_OrderEntry")
and the smell continues on and on.
Again: Whenever your eyes drop on
Dataset, DataTable, DataRow - immediately stop coding, open the ObjectBrowser, and learn, what typed Alternatives the Dataset-Designer has generated for you.
You have learnd to know Databinding as a magic to present Datamodel-Data by synchronisized Controls. Means: What-you-see-is-what-you-get, and it can no longer occur, that you have a list of Data, and your Listbox displays something else. Even if several controls present the same Data in different Views, changing it with the one Control changes at the same Time the other Controls Presentation - they are bound.
That works perfectly on a Form, but often we need more than one Form. For instance this articles sample-Application needs four Forms, namely
frmMain and three Dialog-Forms, to support the User to edit Customer, Articles and Orders in a safe and user-friendly way.
But while designing other Forms using Databinding, we encounter a serious Problem:
Their Controls bind to other typedDataset-Instances :wtf:.
The Form-Designer can't help that: Drag an Entity from the Datasource-Window on a Form - it generates a typed Dataset, and configures Bindings - like a charm :thumbsup:. Do so with two Forms - and you have two Datasets :thumbsdown:.
And that's a mess (gently spoken). Because when at Runtime you open Form2 - there is no Data :wtf: (which is in Form1).
Now loading the Data twice is still worse, since then you propably will have some Changes on Form1 and other Changes on Form2. There is actually no way to save back both Versions in a satisfying manner.
DataConcurrency-Conflicts - Databasings worst case.
The solution lies in the "Highlander-Principle": There can be only one! ;)
Only one typed Dataset, for the whole Application - no matter how you achieve that.
Yes, that's complicated, since it requires to change-plug each BindingSource, from the second Dataset to the Form1-Main-Dataset-Instance.
In particular theese change-plug-riots are not that horrible, it only affects Top-Level-BindingSources - others, which are plugged to other BindingSources "inherit" the plug-change from their Parent-BindingSource.
Moreover you can reorganize all Bindings in Designer, and connect them to a "Main-BindingSource", so at runtime you only have to change one single BS.
But in general, the purity and elegance is badly damaged, since that drag and drop binding-building does not work so easy anymore.
Change-plug Riot by Reflection
I created a solution, where Refection scans the whole Form, and change-plugs each found BindingSource being in line. The code is quite difficult, so I encapsulated it within a Extension-Method, named "Register()", and now the challenge is left to you, to import my Helpers-Project properly.
If you achieve that you can continue build bindings as used, and the call
Me.Register(myDataset) eliminates all evil concurrent typed Datasets, and change-plugs all BindingSources to the one and only "Highlander-Instance".
Please check it out: Download the Sample-App, start it, click Menu "OpenOtherInstance", and execute changes in one of the Forms.
And enjoy, how the other Forms Display follows :D.
There is a WinForms-Feature, also rarely known, and might also worthwhile for its own Tutorial: Namely how to build Standard-Dialogs. A Dialog usually provides the following Features:
- Before Opening, Default-Data can be set
- Opening is modal - means: the user must close the Dialog explicitely to return to the Main-Application
- User can input Data or change the given default-Values
- One can cancel the input
- Afterwards the Input is available for further processing
This pattern is perfectly integrated in the WinForms-Design-Support: You can design any Form, give a Cancel- and an Ok-Button, and set their
.DialogResult - Property to
DialogResult.Cancel / .Ok.
Then set the Forms
.AcceptButton-Property to the Ok-Button, and its
.CancelButton to the Cancel-Button, and you're done - all in Designer, no line of Code.
I built a Template-Form for that, which I can copy-drag from my Helpers-Project to the current Project, rename it and immediately start to design its Contents:
dlgArticle, simpel DatailViews as explained before, placed on thoughtful configurated TableLayoutPanels:
The third, more complex -
dlgOrder - you already know from above, but now see its Design:
And keep in mind a single, most important general rule: Don't process User-Input within the Dialog itself.
Processing the Users Dialog-Input is concern of the Dialog-Caller, since he holds the context, in which the input is to integrate.
It is corruptive, since the Dialogs CodeBehind is so empty, while the Main-Form may crowd over. But don't un-crowd it by perpetrating an architectural misdesign ;).
Doing things twice is bad style and doing complicated things twice is very bad style. That's why I use to include one (or even more) Helpers-Project into Applications I develop.
The here attached sources contain a small Version of it, but I'm afraid, big enough to let some Beginners desperate :(.
It mostly contains generic Extension-Functions, mostly self-explainatory, but some stuff is not so, and is quite advanced.
Other stuff - especially Debug-helpers - may seem obscure, since un-used at all, since meanwhile I removed their Usages.
The most complex and obscure Function of it I have already mentioned:
Form.Register(Dataset), which scans the Form with Reflection and enforces the "Highlander-Principle" to the typed Dataset
The next advanced are
BindingSource.EditCurrent(Of T As Form) and
BindingSource.EditNew(Of T As Form), which both do mainly the same:
- Create a
T- Instance - note:
T derives from
- Register its Dataset, to enable form-transcending Databinding
- Scan the
T with reflection to find an appropriate Destination-BindingSource
- Set the Current Record of the Caller-BindingSource as Datasource to the Destination-BindingSource
- Open the
T modal, and return the
.ShowDialog()-Result to the Caller
The approach is tricky: Instead of populating the Dialog with several particular Default-Data-Values I simply assign one single Record (emanated as
DataRowView), and Databinding does the rest.
IEditableObject, the option to cancel keeps granted, without any effort of mine.
Using this I can handle four User-Experiences, each with only one line of code: 1) Add, 2) Edit a Customer, 3) Add, 4) Edit an Article.
Experience 5) and 6) - Add/Edit an Order - is too complex for an One-Liner - as seen that takes 20 Lines (and that might be the most elegant solution ever, to that particulary problem)
An Excercise, left to you, if you like
As seen there are Add/Edit - Dialogs for Articles, Customer and Orders. How about, if you would attach User-Experience the same way to Add/Edit Categories?
Basically that is very simple - just replicate eg the Customer-Dialog. But you also can take the challenge to implement the CRUD even to the
Category.Image-Property (using FileOpenDialog and stuff).