Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ViperWorks.Ignition WinForms Framework

0.00/5 (No votes)
7 Jun 2010 1  
A WinForms application framework for rapid application development
Title:         ViperWorks.Ignition WinForms Framework Overview
Author:        Tyron Harford
Email:         tharford (dot) nz [at] gmail {dot} com
Member ID:     451298
Language:      C#, VB.NET, .NET 4.0
Platform:      Windows
Technology:    WinForms, WCF, WPF (some)
Level:         Intermediate
Description:   A WinForms application framework for rapid application development
Requirements:  Visual Studio 2010, DevExpress 2010

Downloads / Links

  • The Source - version 5.0.1005.31 (approximately 40MB - 7zip file)

   That link will take you to CodePlex where you can download the latest version.

  • ViperWorks Blog - http://viperworks.wordpress.com

   That link will take you the Ignition blog - that will be kept up to date with any activity.

Updates

03 June 2010
- Added updates section.
- Changed Licence to Microsoft Public License.

08 June 2010
- Added link to blog.

About Me

I'm 27, I like long walks on the beach. Waitttt a minute, it's not that type of article :). I thought I'd write a quick note about me as well. When I look at articles, I tend to look at the profile of the person - I like seeing or understanding people - kind of makes you understand a little more about the article really. I've been programming for quite a while now, my first ever experience was a TI-85 graphing calculator - I got the calculator, well, because it was big and it could graph! How Cool! Little did I know that you could actually program on these devices. So I got into programming on them and found it was pretty darn cool. I then found about this tool called QBasic. My first ever computer program was some Pythagoras Theorem program about triangles. Wasn't the most fancy program, but it was my first. I was a bit disappointed to find out the code generated had to be run in QBasic!

Fast forward a couple years (bout 16 I guess) and here I am with my brand spanking new version of Borland C++ Builder. My first real Windows development came with this tool and it was cool. I created a Monopoly game and my crown jewel of the time, an encryption/decryption algorithm. Only encrypted text files and wasn't anything like public/private key encryption, but it did use different rounds of encryption and you could encrypt the same document twice with different results. Took me about a year to write that. Could write that now in 2 days flat. How things change!

Fast forward a few years more if you will, here I am sitting in my first programming job (bout 21). Life was awesome. Thought I had everything figured out. Great job, nice car (well, nice being a '77 2.0 liter Ford Escort - 5 speed manual no less). I was introduced to .NET programming. The company more or less was into VB.NET programming (VS2003) and I loved it. Took a while to get that love, I was a bit nervous with the .NET framework and my first project (commercial) is still used to this day. To really understand .NET, I ended up writing two versions of that project. One in VB.NET and one in C++ Builder. Needless to say the .NET way was substantially quicker.

So here I am. Two thousand and ten the year is. I no longer have that car (wrote it off in an accident). I've now got a motorbike, have travelled the length and breadth of New Zealand I have, and plan todo the same for Europe, Australia and the USA. Maybe even Africa. I've been programming for about 11 years now, commercially for 6. That's not a long stretch by any means of the imagination, but 6 years is a long time in one company. 5 of those 6 years I spent nearly every waking hour outside of work, working on Ignition. Picture that. Go to work, work your usual stuff and then go home, do your own work. I know I'm not a unique case, but its hard work sometimes, to keep your head up and keep going on the project. You start to have no social life and you start to question if this is the right thing to do. I guess it depends on how much one analyzes themselves to ask these questions.

I'm immensely proud of Ignition, it's the biggest thing I've ever worked on and I've spilt blood, sweat and precious metal over the project. Blood being breaking my hand, sweat because of a sweaty office and metal? Well, I guess I lost my first car during this time, does that count? Before this turns into a slightly odd biography, I'll stop here. I could go on and on, but that's a little about me!

Introduction

Ignition is a WinForms framework, designed for rapid application development. Instead of creating full blown WinForms applications from scratch, Ignition serves as a framework, providing database access, reporting, scheduling, webservice access and one of the best interfaces in the WinForms world to help get your application off the ground. Ignition uses DevExpress. Extensively. To recompile most of the code, you'll need DevExpress. There is also not many (if any) code examples in this article.

The concept of Ignition is a WinForms shell. The shell has the common elements such as reporting, navigation - all the required parts of a standard business application. It has buttons like New, Delete, Save, Refresh, Load. And Search. When you design screens in Ignition, you inherit from a common screen (user control). You then override those commands (New, Delete, Save...). So your screen doesn't have much in it - just deal with the business process. Ignition takes care of the rest.

Ignition is about 95% complete, but still needs work. At the bottom is a list of work that still needs to be done. As I will say multiple times, this project is big. Very big. I can't cover everything in this article, so if you have any questions at all, post them, and I'll try and answer them.

Installation

The easiest way to get started is:

  • Make sure you have something like 7zip to extract the download.
  • Make sure you have .NET 4.0 installed.
  • If disabled, turn on the Net.Tcp Port Sharing Service (on the server - doesn't have to be started on client machines).

I haven't included an all-in-one installer - just because the system is reasonably easy to start using. Once you've downloaded the source (which includes the binaries), extract to location of your choice, then:

  • [Your Location]\ViperWorks\Binaries\ViperWorks.Ignition.Reactor and start the Console EXE
  • [Your Location]\ViperWorks\Binaries\ViperWorks.Ignition.Client.Explorer and start the EXE
  • Login with admin (no password) or demo (no password)

In the server folders, there is also a Windows service EXE. Install this the usual .NET way to get a service instead of a console. Using the console can be great just for testing the waters. You don't need VS 2010 or DevExpress 2010 to run it, but you will to recompile it. You can use the evaluation of DevExpress 2010 but you'll get a whole lot of annoying demo notices.

To make things even clearer, here's a pretty picture which has the layout:




Here's the structure. So exciting! (I had too much sugar this morning).

So at the root, you have ViperWorks. Under that you've got Binaries and Source. Under binaries, you have all the binaries (bet you didn't see that coming) for the client and server apps. For the server apps, you always have two EXE's. One for the console mode, one for the Windows service. Use the batch file to install the services. There is also a Utilities directory. The Utilities directory has a key generator app for Ignition as well as WebServiceStudio for web service testing near the bottom of the article. You may also see that there are two Fusion directories. One has a .Basic extension on the dir. Now both services do the exact same thing and listen on the same ports, so use one or the either. The .Basic version is setup to run with basic bindings only. We will use this one for webservice testing later on with WebServiceStudio.

And then you have the source folder. Under that, you've got references - this has all the main references the source code uses, except DevExpress references. Those references come from the GAC I believe. Under source you have another folder called ViperWorks, and under that you've got Controls, Core and Ignition. Controls and Core contain all-purpose assemblies, and the Ignition folder contains the source to Ignition. I've put all the Ignition projects in one solution. Mainly for debug purposes, but you could seperate this out to make it easier to manage/maintain.

The supplied database is in the binaries folder, inside the Reactor folder, subfolder Data. It's an MDB file. If you want to try MSSQL, just use the upsize tool in Access. Don't add timestamps or anything like that, just do the structure, indexes and data. I didn't include the MSSQL files because of size. If people really want it I can upload the MSSQL backup or the MDF/LDF files.

You may need to open up ports on your Firewall as well. These are used:

- 61555 (UDP) (all applications)
- 8810 (Reactor TCP)
- 8820 (all health WCF contracts)
- 8830 (Fusion HTTP)
- 8840 (Fusion TCP)

If you are running Vista or Windows 7 and have UAC at a medium or tight level, you may get problems with Fusion binding to port 80 if you change that. Either implement the required rules in Windows or run with no UAC.

Another really important note is that Ignition is designed to run on a network. If you are doing local testing, you shouldn't have any problems on a Windows Vista/7 machine. If you are on Windows XP/2003 and you have no valid network connections, you will need to create a loopback adapter for Ignition to work. See Google for how to do this. If you have started Reactor and the WinForms client, and have no environments in the drop down box - you'll need to create the loopback adapter. On a side note, if you have ever worked with Virtual PC's on a laptop without a network connection getting data to and fro can be a right pain using the network. Just create a loopback adapter to fix that problem and you have an ad hoc network.

ViperWorks

Why use ViperWorks in the namespace? It sounds cool. So cool, there is no word to describe it. Even cool is afraid of ViperWorks. Seriously though, the name came from Lockheed SkunkWorks. If you've never heard of the SR-71, go check it out. That's why I called it ViperWorks. The SR-71 is JetFire in the second Transformers movie by the way. I was going to call my company this name. But things change. I also wanted a professional look to the naming of assemblies.

This plane is the fastest ever air-breathing plane ever created. And she was born in the late 60's. Makes me wonder if we've lost our way with innovation sometimes.

Who should read this

WinForms frameworks aren't everybodys cup of tea. Technology seems to be heading back to main frames. The web for some reason attracts the most developers. Ignition is a WinForms frameworks - although not written, I have done prototype testing with XBAP's. Making the possibility of hosting in the web very real. If you want to see a possible way to write a framework (I admin, may not be the best way, but it's a way), check this out. There is a heap of code in here, so just download, check it out and see what you think.

If you want to read about WCF, continue on. If you want to read about multicasting in a real world scenario, read on. If you want to see how to dynamically load classes from assemblies, read on. If you want to see a way of creating a system that is database independent (so far with MSSQL, MySQL and Access support), guess what.... read on.

The system is big. Plain and simple. It's huge. This is the biggest project I have ever undertaken. And I'm dammmnnn proud of it to boot. It has many different technology examples in it. Only problem is... did I mention it's big? Overtime I've forgotten what I did, and it may have fallen through the cracks. But the code is here for everyone to read and analyze.

A couple of cool features in it:
  • An implementation of licensing
  • Basic implementation of runtime multi-lingual support (meaning language support is not hard coded in DLLs)
  • Use of multicasting to broadcast service information - removing needs for config files
  • Runtime class loading from assemblies
  • Implementation of database independence
  • How to tightly integrate help (as well as user added help)
  • How to use the DevExpress skinning
  • And many other examples

What you can do

So you know a little bit about Ignition, but what can you do with it? Ignition was designed from the start to abstract the database - that's the primary use of it. Say you're developing a restaurant application. You want to have screens that manage staff, menus, bookings and customers. To do this part you want a professional WinForms application to manage these parts. But then you want a touch screen interface (maybe WPF) that lets staff assign customers to tables. Maybe this app isn't part of Ignition, but you want to use the business logic. Just design a separate app, use the business logic via webservices exposed using the Fusion service (more detail on this included service later) and that's the Point of Sale system done. But maybe the business gets bigger - you want customers to book tables online. Although Ignition doesn't have a web front end, you could easily hook into the webservices, but only expose the basic webservices externally.

That's a basic example, the framework is designed to get you started and do the dirty work for you. Doesn't do everything, but a large portion of it.

Background

The story of Ignition comes from many moons ago. Not sure how many moons exactly, but it's about five years in the making. It originally came from a spare-time project at work - we had a backup system in place consisting of batch files. Basically these batch files ran each night, copying files across the network to a central location. NTBackup would run after that, putting all the important data onto a tape drive. Now, at the time, I thought "This is pretty old school - there must be a better way!". I was pretty naive back then (still am, just with different things these days) and thought a .NET app would solve all my problems. We still use those batch files.

But, during that process, I wanted to keep a database of the different computers that would need to be backed up. I got sick and tired of writing TSQL to perform CRUD operations, so set of writing a way to automate this. ORM frameworks hadn't really hit the market back then. If they did, there were much too complex, still in the infancy stage, or beyond my understanding. So I rolled my own. I got it working, was pretty darn cool - basically fire a class into this code, and the class would be analyzed and then the relevant TSQL would be generated (ie. INSERT, DELETE, SELECT, etc...). Worked a charm.

Now the other thing I observed was that in the company I noticed that at this point, we had 3 .NET apps in our suite. And one VB 6.0 app. Don't get me started on that one. But all these apps had different ways of getting data, they all looked very different and the user interface in some of them just didn't make sense. So I thought, there must be a way to solve this. At this company, we also have a very powerful 4GL. Now I didn't have the pleasure of meeting this person, but he created this powerful 4GL from scratch. This was pre-Windows days. I thought, this 4GL is cool, but it's way too restrictive. You can't jazz up screens or do anything really cool. Having this restriction makes sense though - all screens created look more or less the same - no matter what developer wrote the screens.

So I set about writing my own framework - not as advanced as the one I looked at for inspiration in terms of functionality, but it was more flexible. Five years later and after many ideas, most implemented, some not (see end for what's not done), here's the result. Initially, it was a server/client system using remoting. Then it went to WCF. Then I added in a scheduler service to automate certain tasks. Then I added in a service to dynamically host webservices. Then I added in tight security to that service for webservice hosting. Then I thought about notifications of modified data. Then I thought about the main WinForms client some more and added skins. Then I thought about corporate branding. Then I thought about multi-lingual ability. Basically the list goes on and on.

You may remember seeing a project a couple years back called Draco.Ignition. See the open source paragraph below as to the story behind this. This project has evolved after many years of work (the very, very first iteration was based on the Pet Shop example from Microsoft - it's that old - in technology years).

So in a super small nutshell (like, smaller than a peanut shell - like an M and M shell perhaps?) that's the story of Ignition. Obviously not the most interesting story, but it's a story. Doesn't equate to anything like climbing Mount Everest, but it's a story other developers may relate to. And the project has heaps of cool stuff in there. This is part zero, an introduction if you will, other parts will follow, not on everything, but on the major aspects of the framework. This isn't really a code article like the usual, it's a story of how the framework came to be.

The system is also database independent. It runs on MSQL, MySQL and Access. Most of my development has been on Access. I did this, not because I have an affinity for MDB files, but because it's the lowest common denominator. What I create, has to run on Access. It serves quite well for creating a DB independent system. That's not to say you can't use more advanced features. DB independence is done via interfaces, so the implementation can be however you want. For example, I havn't used spatial functionality in MSSQL, but you could put it in. And then do a workaround or something like it in Access. Or if you don't ever see yourself working with MDB files, just through a not supported/implemented exception.

Why Open Source?

You may be asking, why spend five years on a project, a marketable one at that, and release everything for free (to a degree)? It's because I'm nearly just over this project. Five years is an eternity in this technology focused world. Five years is a long time in the real world. Things change. You can get married, you can have children, you can do many things in five years. You learn. Your priorities change. You buy a motorbike and travel a country. You start realizing technology isn't the only answer to problems. You start asking yourself questions and end up writing in short sentences. Like this.

I basically want todo other things. I want to get back into photography. I want to try different jobs. This project ended up being like an addiction. Every waking moment outside of my paying job in nearly the five year span was spent on this project. It gave me knowledge, it gave me near insanity (those things are so closely related). Or at least made the insane problem worse.

Now I got very close to running a business, complete with a support website and awesome infrastructure. I think it would have been a blast. If I were truly interested in my life going down that track that is. Being a programmer and creating applications from words is one thing, running a business is something else. Especially in the IT realm. I would have sold Ignition as it is to other developers, or failing that, I would have created some applications to put inside Ignition and sold them. But I asked myself one night, "Do you want to do this. Do you 100% want to sell this product. Would you love doing it?". And I realized I didn't want to do it.

So here I am, making it open source. The other option would be to keep it on my computer and never show it to the world, but I didn't want to do that. I want people to learn. The only reason why Ignition exists is because of sites like CodeProject and the open source community, so it's just giving back. What goes around, comes around.

When I say everything free (to a degree) what I mean is that the source is freely available. But you can't take it, rebrand everything and sell it as your own, closed source. Apart from karma biting you in the a$$, it's just not right. If you make changes, and think people can benefit - make it open source.

Other Open Source Work I Have Used

Over the years, I've lost count of all the open source I used. If you see your code in Ignition and I have not mentioned you as a source, I sincerely apologize. Give me a yell, and I'll add your name to the list. With the graphics, I've obviously thought Adobe did a bang up job with their icons, so my icons look very similar. Splash screens, logon screens, logos were all designed by myself - basically everything graphical is my work, except for the non-app icon images. I've used Windows Vista/7 icons as well as the Silk icons (check that package out - very cool stuff).

Assembly:        ViperWorks.Core
Namespace:       ViperWorks.Core.FileIO.CSV
Class:           [Multiple]
Author(s):       Sebastien Lorion
URL:             http://www.codeproject.com/KB/database/CsvReader.aspx

Assembly:        ViperWorks.Core
Namespace:       ViperWorks.Core.Security.Encryption
Class:           [Multiple]
Author(s):       wumpus1
URL:             http://www.codeproject.com/dotnet/SimpleEncryption.asp

Assembly:        ViperWorks.Core
Namespace:       ViperWorks.Core.Shell
Class:           [Multiple]
Author(s):       Mattias Sjögren
URL:             http://www.msjogren.net/dotnet/

Assembly:        ViperWorks.Core.Logging
Namespace:       ViperWorks.Core.Logging
Class:           [Multiple]
Author(s):       [Multiple]
URL:             [this is log4net with modifications]

Assembly:        ViperWorks.Controls.CodeEditor
Namespace:       ViperWorks.Controls.CodeEditor
Class:           [Multiple]
Author(s):       dotnetfireball, valda [dotnetfireball actually is another project]
URL:             http://sourceforge.net/projects/dotnetfireball

Assembly:        ViperWorks.Controls.Input
Namespace:       ViperWorks.Controls.Input
Class:           [Multiple]
Author(s):       [inputsimulator]
URL:             http://inputsimulator.codeplex.com

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.Calendar
Class:           [Multiple]
Author(s):       Dinesh Chandnani
URL:             http://www.windowsforms.net/Samples/download.aspx?PageId=1&ItemId=222&tabindex=4

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.Charting
Class:           [Multiple]
Author(s):       mattsj1984
URL:             http://www.codeproject.com/cs/miscctrl/PieChart.asp

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.General
Class:           BindableListView
Author(s):       Ian Griffiths
URL:             http://www.interact-sw.co.uk/utilities/bindablelistview/

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.General
Class:           GradientPanel
Author(s):       Mark Jackson
URL:             http://www.codeproject.com/vb/net/custompanel.asp?select=931204&df=100&forumid=73173&fr=51
                
Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.General
Class:           LoadingCircle
Author(s):       Martin Gagne
URL:             http://www.codeproject.com/cs/miscctrl/mrg_loadingcircle.asp

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.General.Renderers.Office2007
Class:           [Multiple]
Author(s):       Phil Wright
URL:             http://www.componentfactory.com

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.PerfChart
Class:           [Multiple]
Author(s):       eclipse2k1
URL:             http://www.codeproject.com/cs/miscctrl/SimplePerfChart.asp

Assembly:        ViperWorks.Controls.Core
Namespace:       ViperWorks.Controls.Core.Thermometer
Class:           [Multiple]
Author(s):       Niel M.Thomas
URL:             http://www.codeproject.com/KB/miscctrl/ThermometerControl.aspx

Assembly:        ViperWorks.Controls.Compression
Namespace:       ViperWorks.Controls.Compression
Class:           [Multiple]
Author(s):       Mike Krueger
URL:             http://www.icsharpcode.net/OpenSource/SharpZipLib/

Assemblies

It helps knowing what the different assemblies are used for, so here's the list (alpha sort):

ViperWorks.Controls.CodeEditor (C#)
This is not my creation. It comes from dotnetfireball (which comes from something else). It's basically a control that provides syntax highlighting. Used in a couple screens.

ViperWorks.Controls.Controls (C#)
Partly mine, partly other open source work. Contains general UI controls.

ViperWorks.Controls.Input (VB.NET)
Handles input from controls. Contains open source work. This is basically the InputSimulator available from CodePlex. Not used yet in Ignition, but the main idea was to get the mouse back and forward buttons for navigation. You can't easily do this using the normal mouse events.

ViperWorks.Controls.Xbox360 (VB.NET)
Handles input from Xbox 360 controllers. Based off work on a CodeProject article.
Not used in Ignition, but I do have a project for my own personal use that uses this.

ViperWorks.Core.Compression (C#)
Handles file compression. Not my work.
Not heavily used in Ignition, only for help import/export.
Would be used if functionality export makes it in.

ViperWorks.Core.Data (VB.NET)
Contains data implementations for MSSQL, MySQL and Access.

ViperWorks.Core (VB.NET)
Contains core libaries. My own work and others.

ViperWorks.Core.Logging (C#)
Contains logging code (essentially log4net)

ViperWorks.Core.Office (VB.NET)
Integration with the Word document format.
Note heavily used in Ignition.

ViperWorks.Ignition.Core (VB.NET)
Core library used by all Ignition applications.

Applications

It helps knowing what the different applications are used for, so here's the list (alpha sort):

ViperWorks.Ignition.Client.Explorer
This is the main WinForms application. This is essentially Ignition.

ViperWorks.Ignition.Client.Plasma
This is an admin tool, there were greater plans for this, like installing/uninstalling services - stuff like that. Right now, you can use it to see what the services are doing.

ViperWorks.Ignition.Firestorm
This was to be the XBAP/ASP.NET application for hosting Ignition in a web environment.

ViperWorks.Ignition.Fusion
This application hosts webservices from your code. Very cool stuff, via reflection it looks in your assembly and creates a wrapper around your code. Tight security is included.

ViperWorks.Ignition.Modules
This is a bunch of DLL's, your application goes in here.

ViperWorks.Ignition.Reactor
This is the service that does the data work.

ViperWorks.Ignition.Scheduler
This is the service that performs scheduling of requests.

The Client

Right, with all that stuff out the way, onto the good stuff - screenshots!


This is the logon screen for Ignition.


This is the screen you get when you start Ignition.


This is an example of a report.


This is an example of a more complex screen.


This is an example of a more complex screen but with a different skin.


This is an example of a basic multi-lingual implementation.

So hopefully with those screenshots, you get an idea of what Ignition looks like. It didn't use to always look like that, it's gone through quite a few stages. Should have taken screenshots back then! So the client is a WinForms client. The idea is you create screens and business logic in your own assemblies and use them in the client. All the complicated stuff is done for you - just concentrate on your business logic and basic screen presentation. That UI can be confusing at first - here's a breakdown:


Part A - Navigation
These are your back and forwards buttons - navigation is like a web browser.

Part B - Record Actions
From left to right, this lets you create a new record, open an existing record (this only works on screens that have a user friendly ID field - like a field name), save the record, delete the record, refresh the record, revert any changes made (before save) or add to your favourites. You need an active screen loaded to use any of these buttons. The availability of these buttons depends on your security level (the security in Ignition is role based) and on the setup of the screen (more details on this follows).

Part C - System Menus
The left most button opens up the configuration menu. You can't configure much in Ignition, convention over configuration is the name of the game. The Explorer bar is also doubled in here - just in case you want to access the system that way. You can however:
  • Open up the output window (useful for live debugging)
  • See the daily tip
  • Change the skin
  • Change the language
  • See the About box
  • Send a message (not implemented)
  • Switch to fullscreen mode (use F12 to go back)
  • Hide the explorer bar (Part F)

In the middle is a special button - when Ignition is first shipped - you get the standard way of accessing screens using a tree type structure. You can't easily change this (more on this later). This middle button lets you create a menu structure (including sub menus) that can be applied to the whole organization. Great for when you are not happy with the system default way of showing you functions to get to. Change it to how you want. You can even include file path information (so put that URL in there). Ignition was designed as a framework for a portal to everything else. Kinda like SharePoint I guess, but for the WinForms world.

At the end is the Home button - this lets you close all screens and go back to the home screen. Note that you can set a screen as the home screen, this will take you to that screen.

Part D - QuickSearch
The QuickSearch box lets you get quick access to any screen in the system. The shown example will let you type in a field name and when you hit the magnifying glass, a list of search results will show. If you enter the exact field name, you'll go straight to the screen used to maintain that detail. This is 100% customizable. You could change this to search for contacts in your contact system. Or maybe ID's of students for your student management system. How you change this is further down.

Part E - Explorer Bar
The Explorer bar is used to show the main system menu, show your favourites menu, show what tasks you can do with the current screen, show search options, show the history for the current record, show the help pane, show the attachments and stickies for the screen/record, show reporting options, show the user directory, show the data browser, and show the security for the current user. It's how you use the system. You can hide this (as described above) and get access via the gear drop down.

Part F - Favourites Bar
The Favourites Bar can be used to store your frequent favourite screens, records or reports. It's an easy way to get to common things you do. You also have your full blown favourites list which can be structured into folders, but sometimes you just want the basics.

Part G - Tab Header
Nothing too special here - just a tab header. You can drag/drop tabs. Because Ignition is a tabbed interface, just like the web browsers, you need to get to those tabs. Usually the tabs are created by you, navigating to different screens. There are also system tabs, like Help, Tab Listing (like IE), Search Results, Current Report and a button to create a New Tab.

Part H - Screen Header
The screen header displays information on the current screen - the title of it, what module you are in, the ID of the screen (IE. CORE2014), a Help button and an indicator for the current status (ie. Edit Record). The logo at the far right can be changed via the control panel. Corporate branding is a huge part of Ignition.

Part I - Explorer Pane
This pane changes depending what you clicked on in Part E. This is detailed further on.

Part J - Your Screen
This part holds your user control. Naming should be like CORE2012. Don't have spaces of the usual crap. CORE being the ID of the module. So it would be like SWIM for a swimming module. Whatever a swimming module would do.

Customization

A big part of Ignition is branding. From the ground up, Ignition was designed to be used in corporate scenarios. However it was also designed for the one man band operation. It was designed to grow with a business. Maybe you're a developer needing a framework to manage your business. You want to write some invoicing system as well as mass-emailing. You can write that functionality in Ignition with minimal fuss. You have your own logo - you can put that in the startup screen. You can replace the logon screen (background image). You can replace the splash image. Crikeys, if you have DevExpress 2010, you can use their tools to create your own skin assembly and use that assembly for your skin. But you might not want to do all of that right off, maybe in time you want to brand it how you want - the option is there.

Same thing with the DB. Maybe you are a one man band (even like a builder) and you can run off an MDB to start off with. But then you get some help and find the MDB doesn't cut it. You decide to take the MySQL approach. Then you find you are a multi-national company and need to-do fancy reporting and decide to go MSSQL. You can do this with Ignition.

I realise you can do this with many applications these days, but keep in mind alot of the core functionality was written when most of these tools didn't exist (like Entity Framework) or that new MVC pattern stuff.

Also - in the client directory, there is a directory called Branding. If you stick in PNG's in there (example supplied), you can override the logon screen and splash screen. Format is like "DEMO.Logon.png" where DEMO is the environment. The other one is "DEMO.Splash.png".

The Explorer Panes

The Explorer panes form a fundamental part of Ignition. Here's what they are:

Menu
The Menu pane forms the way in which you access Ignition. Depending on your security, this may look different for different users. The Menu is one way of accessing screens. The other ways include Favourites and your custom menu (check out Part C above). You can right click on items to open in the active tab, a new tab, or a new window. You can also add items to your favourite list. And you can set your home screen as well. If you are an admin, you can customize this menu by adding your own folders and items. Perfect for integration scenarios.

Favourites
The Favourites pane lets you organize your favourites in a tree like fashion. Right clicking enables you to open in a new tab, active tab, new window, delete the favourite or pin the favourite to the favourites bar. Favourites are per user.

Tasks
The Tasks pane is separated into two parts - the top part lets you perform record or field tasks. Record tasks being searching. Field tasks being opening up a multilined textbox in a bigger window, or spellchecking. More info on spellchecking below. The bottom pane is related tasks. Say you have a Invoice system. If you had a screen that maintained invoices, you may want to navigate to the contact for that invoice. Thats what tasks are for. Tasks join the system together. Tasks can also be inter-module so you can combine your modules together and provide seamless integration.

Search
The Search pane is also separated into two parts - the top part lets you select the type of search. Different screens may have different ways to search for data. If you had a contacts system, you may want to search by address or search by surname. You setup your searches via metadata (also, see below). The second box provides parameters for the search selected. Some searches don't have any parameters - in this case, the second box doesn't do didly squat.

History
The History pane shows you a list of modifications to the record you are looking at. The amount of detail will change, depending on how you set the screen up. In some cases you don't want any transactional history, in some cases you want to only record that something changed. In other cases you want to see what fields got changed. This pane lets you see that information. More info as to how to set this up is later on.

Help
The Help pane does exactly that - shows help. Within the Help pane are three tabs. One tab lets you view in a tree structure the system help. As of uploading, there isn't much there. The second tab shows field help. If you are on a screen and press F1, this is what you'll see. The third tab lets you search the help. Help comes from metadata. When you create the metadata for a table or field, or core system component, you can create help in HTML form. End-users can also add their own notes, this data is separate from the core help and can be removed by admins.

Attachments and Stickies
The Attachments and Stickies panes lets you add attachments to a record (so your Excel report). Stickies lets you add notes to a record. Just important info, but not so important it forms a part of the data.

Reporting
The Reporting pane (more on this in a separate article) lets you view the reports for a screen. If you are on an invoice screen, you may want a report setup that is the print version. You can also setup chart reports, pivot reports, gauge reports, CSV reports, schedule type reports, web links or grid reports. It's a big part and is described separately.

Directory
The Directory pane is a complete listing of all users. This is designed for quick searching of users in the system.

Directory
The Data Browser will not actually open a pane, it will open a data browser that you can use to search the system. With the results found, you can double click on a record and it will open up in the related screen(s).

How does it work? (Data)

Right, so we've gone over what it is (a WinForms framework put simply) and we've gone over the basics of the interface. But how does it work you ask? Good question.

As with most applications, it starts at the database. In the database, there are quite a few tables prefixed with XSYS. There are the system tables and these tables drive Ignition. Note that I haven't implemented relations yet. This does make it harder to see how the system is connected, and I'll try and fix this later. By default, I've shipped Ignition with an MDB. But if you want to try MSSQL, just upsize the MDB. A broad overview:

XSYS_ATTACHMENT
This stores attachments you have on records.

XSYS_BUSINESSAREA
A Business Area is a component that sits under XSYS_BUSINESSMODULE. You could consider an Area like a subsection and a Module like a Section. Generally a module is an application. There is the CORE module, this module performs all core functionality - it drives Ignition. Underneath CORE you have things like DATA - data is used for data manipulation. You also have SYSTEM. System drives system related functions. Obviously.

XSYS_BUSINESSFIELD
Fields are metadata on top of columns. They duplicate some things like size and type, but also provide other things like help, column names, values allowed and so fourth.

XSYS_BUSINESSINDEX
Indexes are a recent addition. Basically they add support for DB indexes. The plan was to store the metadata in Ignition, so when you import/export functionality, Indexes can be then built via TSQL.

XSYS_BUSINESSINDEX_COLUMN
These are the columns of an index.

XSYS_BUSINESSMODULE
Modules are your applications. One record per application. When you create a module, you create two assemblies. One for the business logic, one for the screens. Underneath modules you have areas (as above).

XSYS_BUSINESSOBJECT
Objects are the nuts and bolts. Looking back, this should be called Business Function or something like that. Each screen you go to in Ignition is a business object. Same goes with processes.

XSYS_BUSINESSOBJECT_CHILD
This table contains any child objects for the given object. This is used when you have menus. A menu is still considered a business object, but it has child objects attached to it.

XSYS_BUSINESSOBJECT_TASK
When you create an object, you sometimes create tasks (so an invoice screen links to a contact). You define this relationship through tasks.

XSYS_BUSINESSOBJECT_WEBSERVICE
This is a complicated one. When Ignition was first created, it was designed for WinForms. When .NET 3.5 came out, I decided to investigate WPF. Overtime, it was slowly put in, but WPF works slightly different than WinForms. WinForms can talk directly to the business logic. But WPF is designed to communicate with the webservice layer (how it should be). When you load a WPF screen, I need to know what webservices that screen needs. That's what this table is for.

XSYS_BUSINESSRELATION
Same concept as the index table, but used a little more. It's goal was to be used as metadata so when importing functionality, relations can be built in the DB. It also serves a purpose in the report writer. You can have master-detail reports, but for that, you need to know the relations between tables. Sadly not all the relations are created, a later version will remedy this.

XSYS_BUSINESSTABLE
This table stores... Tables. Apart from being a container for fields, this also stores class name info for code generation. Yes - this is a screen in Ignition you can use to generate code, making development even quicker.

XSYS_CLIENTEXCEPTION
Because handling exceptions from clients in a live environment can be a right pain, this table can store all the exceptions that client applications generate. Nifty.

XSYS_CUSTOMMENU_*
This table stores all custom menu folders and items.

XSYS_DATACOMMAND
This table stores data commands you can execute.

XSYS_DTX_*
These tables store data transfer information. Can be used to import/export data.

XSYS_EMAIL*
These table stores a history of emails and attachments sent from the application.

XSYS_FAVOURITE_*
These tables store all user favourite information.

XSYS_FUSIONBINDING
This is to-do. Basically when using webservices in Ignition, you need to know how to bind to them. Data is entered into here as to how to bind. An application that was going to get created was going to be called Firestorm. This application could have been externally facing. Because of this you may have wanted SSL certs, so a different binding would have been required.

XSYS_LANGUAGE
This table stores the different languages available. It also links to the spellcheck table.

XSYS_LANGUAGEWORD
Multi-lingual support is essentially limited to word replacement. You define what words here.

XSYS_MESSAGE_USER
The Ignition messaging sub-system stores messages in here.

XSYS_ORGANIATION*
These tables store organization information and structure.

XSYS_QUICKSEARCH
This stores any quicksearch definitions you have (executed from top right in GUI).

XSYS_QUOTATION
Stores all quotes seen on the startup page.

XSYS_RECORDLOCK
This table stores all record locks. For example, when you open a record, click into a field, a lock is placed, preventing other users from editing data.

XSYS_REPORT*
These tables store all report information. Executed via the Reports pane.

XSYS_ROLE
Stores all roles in the organization.

XSYS_SCHEDULE
Stores all schedules that should be run. For a schedule to run, the Scheduler service needs to be running.

XSYS_SCRIPTING
In Ignition, you can create scripts that execute and return data. You can use this scripts in searches. Sometimes the built in searching doesn't cut the mustard, and even TSQL (yes, you can use custom TSQL in searches) doesn't work. That's when you can use scripts. A working example is when you search for security items.

XSYS_SEARCH*
These tables are probably the most important - they drive all searching in Ignition. Without these, you wouldn't be able to find records. This is so important, it's described fully later on.

XSYS_SECURITY
Security is based on roles and View, Add, Change, Delete, Deny access. Security can be applied at Module (highest) level, Area (middle) or Object (lowest level) level.

XSYS_SETTING
Sometimes a whole table is overkill for storing data. Like the users choice of skin. The setting table stores setting such as this.

XSYS_SPELLCHECK
When you are in large fields, you want to spellcheck. Because Ignition supports multiple languages (though I haven't got around to doing the translations), you need to support multiple spell check languages. Data is stored in OpenOffice format.

XSYS_STATISTIC
For the bean counters. Ignition stores statistics on usage. System stats are stored here, and can be view in a screen in Ignition.

XSYS_STICKY
This table stores any stickies the users create.

XSYS_SYSTEMHELP
Along with the help you put on Modules, Areas, Objects and Fields (and settings), you can create your own help. Some examples are already in there on how to create tree like doco.

XSYS_SYSTEMHELP_IMAGES
Sometimes images speak louder than words. Store any images along with doco here.

XSYS_SYSTEMHISTORY
Earlier I mentioned Ignition can keep transaction details for screens. Obviously the data can get big, so this table stores the names of the different tables used to store this data.

XSYS_TIP
Stores the tip of the day data.

XSYS_TRANSACTION
WIP. Certain tables can be configured to write transaction information. This information could then be passed to a service that performs notifications. This is un-completed at this point.

XSYS_USER
This table stores all users in the system.

XSYS_USERHELP
Stores all user added help.

XSYS_USERROLE
Contains mapping of users to roles. Users can have multiple roles.

XSYS_WEBSERVICE
Contains a listing of all webservices that should be hosted.

XSYS_WEBSERVICE_USER
Contains a mapping of which users can access what webservices. Sometimes you don't want all users accessing all webservices. Some may be confidential.

XSYS_WORLDEVENT
Simple table used to store world events.

So... How Does it work again?

We've described in basic detail what the different tables are for, but how does this data travel? Well, it starts off with some data (surprise!). Take this code for example:

    <Serializable()> _
	Public Class BusinessObjectWebServiceObject
        Inherits ViperWorks.Core.Data.GeneralObject

        Private _BusinessObjectWebServiceObjectID As String
        Private _BusinessObjectWebServiceWebServiceID As String

        Default Public Overloads Overrides Property Item(ByVal Name As String) As Object
            Get
                Select Case Name.ToLower
                    Case "businessobjectwebserviceobjectid"
                        Return Me.BusinessObjectWebServiceObjectID
                    Case "businessobjectwebservicewebserviceid"
                        Return Me.BusinessObjectWebServiceWebServiceID
                    Case Else
                        Return Nothing
                End Select
            End Get
            Set(ByVal value As Object)
                If (value IsNot Nothing And TypeOf value Is DBNull = False) Then
                    Select Case Name.ToLower
                        Case "businessobjectwebserviceobjectid"
                            Me.BusinessObjectWebServiceObjectID = CStr(value)
                        Case "businessobjectwebservicewebserviceid"
                            Me.BusinessObjectWebServiceWebServiceID = CStr(value)
                    End Select
                End If
            End Set
        End Property

        Public Property BusinessObjectWebServiceObjectID() As String
            Get
                Return Me._BusinessObjectWebServiceObjectID
            End Get
            Set(ByVal value As String)
                Me._BusinessObjectWebServiceObjectID = value
            End Set
        End Property

        Public Property BusinessObjectWebServiceWebServiceID() As String
            Get
                Return Me._BusinessObjectWebServiceWebServiceID
            End Get
            Set(ByVal value As String)
                Me._BusinessObjectWebServiceWebServiceID = value
            End Set
        End Property

        Public Sub New()
            Me._BusinessObjectWebServiceObjectID = String.Empty
            Me._BusinessObjectWebServiceWebServiceID = String.Empty
        End Sub
    End Class
This code represents a table in the database. Each property is a field. When you create a class that represents a table, you must inherit from ViperWorks.Core.Data.GeneralObject. The GeneralObject is the main object that is used to transmit over WCF into the data service (called Reactor). When you want to update data for example, you call:
Me._ActiveContract.DataInsert(Me._ClientID, MyBusinessObjectWebService.MetaFieldList, Me._TableName, Me._KeyColumns)
The _ActiveContract class is ViperWorks.Ignition.Core.Reactor.IReactorContract. The MetaFieldList property is a property on the GeneralObject which will disassemble the class into a generic list of BusinessField objects. This list is then used to create an INSERT or UPDATE statement (usually via parameters). If I were to pass the class itself, the service that does the data work would need a reference to it. I didn't want that, so that's why the class is deconstructed into an object the data service does know about (GeneralObject).

So what is _ActiveContract? It's a class that contains the WCF contract. This class (well, interface) that forms the bulk of communication in Ignition. Your business logic doesn't need to know how to create an instance. This is done for you by Ignition when it starts up. All you need to know is how to use this class to do your data business. The implementation of this contract is in Reactor.

The Business Logic

If you develop in Ignition, you'll spend your time in three areas:
  • Visual Studio - creating business logic
  • Visual Studio - creating screens
  • Ignition - creating metadata and searches

You'll spend some time in Ignition creating searches, metadata, and the screen (Business Object) so that Ignition knows how to treat your screen you created in code. We'll go over the business logic now and have a look at a more complicated piece of logic:

    Public Class BusinessFieldBus

        Private _ClientID As Guid
        Private _ActiveContract As Reactor.IReactorContract
        Private _TableName As String
        Private _KeyColumns(0) As String

        Public Sub New(ByVal rClient As Reactor.ReactorContainer, Optional ByVal Arguments As Dictionary(Of String, String) = Nothing)
            Me._ClientID = rClient.Client.ClientID
            Me._ActiveContract = rClient.Contract
            Me._TableName = "XSYS_BUSINESSFIELD"
            Me._KeyColumns(0) = "BusinessFieldID"
        End Sub

        Public Function GetPrimaryKeyField(ByVal bField As Data.BusinessField) As Data.BusinessField
            Dim bTable As Data.BusinessTable
            '
            Dim bFieldTable As ViperWorks.Core.Data.BusinessField
            Dim bFieldKey As ViperWorks.Core.Data.BusinessField
            Dim MyFieldList As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Dim MyBusinessField As ViperWorks.Core.Data.BusinessField = Nothing
            '
            Try
                MyFieldList = New Generic.List(Of ViperWorks.Core.Data.BusinessField)
                '
                bFieldKey = New ViperWorks.Core.Data.BusinessField("BusinessFieldPrimaryKey", True, _
                   Data.BusinessField.eBusinessFieldType.Boolean)
                bFieldTable = New ViperWorks.Core.Data.BusinessField("BusinessFieldBusinessTableID", _
                   bField.BusinessFieldBusinessTableID, Data.BusinessField.eBusinessFieldType.String)
                MyFieldList.Add(bFieldKey)
                MyFieldList.Add(bFieldTable)
                '
                bTable = Me._ActiveContract.DataSelect(Me._ClientID, Me._TableName, MyFieldList)
                '
                If (bTable IsNot Nothing AndAlso bTable.BusinessFields.Count > 0) Then
                    MyBusinessField = New ViperWorks.Core.Data.BusinessField
                    ViperWorks.Core.Data.DataHelper.ConvertObjectFromBusinessTable(CType(MyBusinessField, _
                       ViperWorks.Core.Data.GeneralObject), bTable)
                End If
                Return MyBusinessField

            Catch ex As Exception
                Throw
            End Try
        End Function

        Public Function SelectByTable(ByVal BusinessTableID As String, _
           Optional ByVal OrderByFieldOrder As Boolean = False) As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Dim SearchList As Generic.List(Of Searching.SearchInfo)
            Dim OrderList As Generic.List(Of Searching.OrderInfo)
            '
            Try
                SearchList = New Generic.List(Of Searching.SearchInfo)
                OrderList = New Generic.List(Of Searching.OrderInfo)
                '
                SearchList.Add(New Searching.SearchInfo(Enumerations.eCondition.AND, "BusinessFieldBusinessTableID", _
                    Enumerations.eOperator.Exactly, BusinessTableID, Data.BusinessField.eBusinessFieldType.String, True))
                If (OrderByFieldOrder) Then
                    OrderList.Add(New Searching.OrderInfo("BusinessFieldOrder", Searching.OrderInfo.eSortType.Ascending))
                Else
                    OrderList.Add(New Searching.OrderInfo("BusinessFieldName", Searching.OrderInfo.eSortType.Ascending))
                End If
                '
                Return Me._ListSearch(SearchList, OrderList)

            Catch ex As Exception
                Throw
                Return Nothing
            End Try
        End Function

        Public Function Insert(ByVal MyBusinessField As ViperWorks.Core.Data.BusinessField) As String
            Return Me._Insert(MyBusinessField)
        End Function

        Public Function Update(ByVal MyBusinessField As ViperWorks.Core.Data.BusinessField) As Boolean
            Return Me._Update(MyBusinessField)
        End Function

        Public Function [Select](ByVal BusinessFieldID As String, ByVal UseFieldName As Boolean) As ViperWorks.Core.Data.BusinessField
            Return Me._Select(BusinessFieldID, UseFieldName)
        End Function

        Public Sub Delete(ByVal ColumnName As String, ByVal ColumnData As String)
            Me._Delete(ColumnName, ColumnData)
        End Sub

        Public Sub Delete(ByVal BusinessField As ViperWorks.Core.Data.BusinessField)
            Me._Delete(Me._KeyColumns(0), BusinessField.BusinessFieldID)
        End Sub

        Public Sub Delete(ByVal BusinessFieldID As String)
            Me._Delete(Me._KeyColumns(0), BusinessFieldID)
        End Sub

        Public Function ListSelectAll() As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Return Me._ListSearch(Nothing, Nothing)
        End Function

        Public Function ListSelectAllFriendly() As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Dim OrderList As List(Of ViperWorks.Core.Searching.OrderInfo)
            Dim DisplayList(2) As String
            '
            Try
                DisplayList(0) = "BusinessFieldID"
                DisplayList(1) = "BusinessFieldName"
                DisplayList(2) = "BusinessFieldFriendly"
                OrderList = New List(Of ViperWorks.Core.Searching.OrderInfo)
                OrderList.Add(New ViperWorks.Core.Searching.OrderInfo("BusinessFieldName", Searching.OrderInfo.eSortType.Ascending))
                '
                Return Me._ListSearch(Nothing, OrderList, DisplayList)

            Catch ex As Exception
                Return Nothing
            End Try
        End Function

        Public Function TableSelectAll() As DataTable
            Return Me._TableSearch(Nothing, Nothing)
        End Function

        Public Function DeleteAll() As Boolean
            Return Me._DeleteAll()
        End Function

        Private Function _Insert(ByVal MyBusinessField As ViperWorks.Core.Data.BusinessField) As String
            Dim Keys As String()
            '
            Try
                Keys = Me._ActiveContract.DataInsert(Me._ClientID, MyBusinessField.MetaFieldList, Me._TableName, Me._KeyColumns)
                If (Not Keys Is Nothing AndAlso Keys.Length >= 1) Then
                    Return Keys(0)
                Else
                    Return Nothing
                End If

            Catch ex As Exception
                Throw
            End Try
        End Function

        Private Function _Update(ByVal MyBusinessField As ViperWorks.Core.Data.BusinessField) As Boolean
            Dim bField As ViperWorks.Core.Data.BusinessField
            Dim MyFieldList As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            '
            Try
                bField = New ViperWorks.Core.Data.BusinessField(Me._KeyColumns(0), _
                  MyBusinessField.BusinessFieldID, Data.BusinessField.eBusinessFieldType.String)
                MyFieldList = New Generic.List(Of ViperWorks.Core.Data.BusinessField)
                '
                MyFieldList.Add(bField)
                Return Me._ActiveContract.DataUpdate(Me._ClientID, MyBusinessField.MetaFieldList, Me._TableName, MyFieldList)

            Catch ex As Exception
                Throw
            End Try
        End Function

        Private Function _Delete(ByVal ColumnName As String, ByVal ColumnData As String) As Boolean
            Dim bField As ViperWorks.Core.Data.BusinessField
            Dim MyFieldList As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            '
            Try
                bField = New ViperWorks.Core.Data.BusinessField(ColumnName, ColumnData, Data.BusinessField.eBusinessFieldType.String)
                MyFieldList = New Generic.List(Of ViperWorks.Core.Data.BusinessField)
                '
                MyFieldList.Add(bField)
                Return Me._ActiveContract.DataDelete(Me._ClientID, Me._TableName, MyFieldList)

            Catch ex As Exception
                Throw
            End Try
        End Function

        Private Function _DeleteAll() As Boolean
            Return Me._ActiveContract.DataDeleteAll(Me._ClientID, Me._TableName)
        End Function

        Private Function _Select(ByVal BusinessFieldID As String, ByVal UseFieldName As Boolean) As ViperWorks.Core.Data.BusinessField
            Dim bTable As Data.BusinessTable
            Dim bField As ViperWorks.Core.Data.BusinessField
            Dim MyFieldList As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Dim MyBusinessField As ViperWorks.Core.Data.BusinessField = Nothing
            '
            Try
                If (UseFieldName) Then
                    bField = New ViperWorks.Core.Data.BusinessField("BusinessFieldName", _
                       BusinessFieldID, Data.BusinessField.eBusinessFieldType.String)
                Else
                    bField = New ViperWorks.Core.Data.BusinessField(Me._KeyColumns(0), _
                      BusinessFieldID, Data.BusinessField.eBusinessFieldType.String)
                End If
                MyFieldList = New Generic.List(Of ViperWorks.Core.Data.BusinessField)
                '
                MyFieldList.Add(bField)
                bTable = Me._ActiveContract.DataSelect(Me._ClientID, Me._TableName, MyFieldList)
                '
                If (bTable IsNot Nothing AndAlso bTable.BusinessFields.Count > 0) Then
                    MyBusinessField = New ViperWorks.Core.Data.BusinessField
                    ViperWorks.Core.Data.DataHelper.ConvertObjectFromBusinessTable( _
                       CType(MyBusinessField, ViperWorks.Core.Data.GeneralObject), bTable)
                End If
                Return MyBusinessField

            Catch ex As Exception
                Throw
            End Try
        End Function

        Private Function _ListSearch(ByVal SearchList As Generic.List(Of ViperWorks.Core.Searching.SearchInfo), _
            ByVal OrderList As Generic.List(Of ViperWorks.Core.Searching.OrderInfo), _
            Optional ByVal DisplayList() As String = Nothing) As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            Dim ReturnDS As DataSet
            Dim ReturnList As Generic.List(Of ViperWorks.Core.Data.BusinessField)
            '
            Try
                ReturnList = New Generic.List(Of ViperWorks.Core.Data.BusinessField)
                ReturnDS = Me._ActiveContract.DataSearch(Me._ClientID, SearchList, Me._TableName, OrderList, 0, DisplayList)
                If (Not ReturnDS Is Nothing AndAlso ReturnDS.Tables.Count > 0) Then
                    For i As Integer = 0 To ReturnDS.Tables(0).Rows.Count - 1
                        Dim NewBusinessField As New ViperWorks.Core.Data.BusinessField
                        ViperWorks.Core.Data.DataHelper.ConvertObjectFromRow( _
                           CType(NewBusinessField, ViperWorks.Core.Data.GeneralObject), ReturnDS.Tables(0).Rows(i))
                        ReturnList.Add(NewBusinessField)
                    Next
                End If
                '
                Return ReturnList

            Catch ex As Exception
                Throw
                Return Nothing
            End Try
        End Function

        Private Function _TableSearch(ByVal SearchList As Generic.List(Of ViperWorks.Core.Searching.SearchInfo), _
            ByVal OrderList As Generic.List(Of ViperWorks.Core.Searching.OrderInfo)) As DataTable
            Dim ReturnDS As DataSet
            '
            Try
                ReturnDS = Me._ActiveContract.DataSearch(Me._ClientID, SearchList, Me._TableName, OrderList, 0)
                If (Not ReturnDS Is Nothing AndAlso ReturnDS.Tables.Count > 0) Then
                    Return ReturnDS.Tables(0)
                Else
                    Return Nothing
                End If

            Catch ex As Exception
                Throw
                Return Nothing
            End Try
        End Function

    End Class
So that's all you need to do to provide the basic INSERT, UPDATE, SELECT, DELETE functionality. It's not super easy, but it's easier than writing all the required TSQL. That code above will work on all supported databases. It's also not the most efficient code. If you are in a bind and need super efficient code, you can access the actual database connection from code as well (via the WCF contract).

So what do these different screens do?

Out of the box, Ignition comes with a few modules:
  • CORE - this is the driving module behind Ignition.
  • MISC - this is a sample module, not required for functionality but has some cool stuff (like a WPF example).
  • ROSTER - a WinForms example showing how to create a schedule like program.

We're going to go over each of these screens. To explain the framework, it'll help to understand what these different screens do, because changing the data in these screens can change functionality. Basically start Ignition, go to the Menu and expand all the nodes and I'll explain what they do.

CORE2070
Ignition > Data > Export Profiles
This screen maintains export profiles. You can use export profiles to export data. How you export data can be with the export profile itself (limited in some ways) to using TSQL to using a .NET script. This screen has a couple tasks hanging off it. Some tasks include setting up criteria, ordering, output columns and some tasks that let you export all data or just a sample. Samples don't with with TSQL or .NET scripts.

CORE2080
Ignition > Data > Import Profiles
This screen maintains import profiles. You can use import profiles to miport data. How you import data can be with the import profile itself (limited in some ways) or using a .NET script to import data. This screen has some tasks hanging off it including import columns, the actual import and a test import with no data modification.

CORE2116
Ignition > Email > Email Log
This screen displays a listing of all emails that have been sent using the Ignition framework.

CORE2096_EXPORT
Ignition > Help > Export System Help
Lets you export all help in Ignition. Can be used to just update help without updating functionality.

CORE2096_IMPORT
Ignition > Help > Import System Help
Lets you import all help in Ignition. Can be used to just update help without updating functionality.

CORE2098
Ignition > Reporting > Report Maintenance
Lets you maintain reports. This is not finished 100% and needs more work (as mentioned at the end).

CORE2112
Ignition > Scheduling > Schedule Maintenance
Manages schedule data. Schedules are like the Task Scheduler in Windows. Idea is to run your code at specific intervals. Useful for data integration scenarios. Schedules can also be used to run scripts.

CORE2025
Ignition > Searching > QuickSearch Items
Lets you maintain QuickSearch items (top right of UI). QuickSearch items lets you quickly search the system for data. Great for customer service scenarios when they need quick access to any bit of data.

CORE2014
Ignition > Searching > Search Definitions
Search Definitions form the bulk of Ignition. When you are on a screen and need to find data - what do you do? You normally search for the data. Searching lets you define styles (so when a certain row meets a certain condition, you can change the appearance. Like negative dollar amounts). You define the ordering, grouping, criteria and what gets shown. Searches can use TSQL and .NET scripts. This is a big, big part, so any questions on this, give me a shout.

CORE2040
Ignition > Security > Manual Security
Lets you define manual security for screens, as well as custom menu items.

CORE2034
Ignition > Security > Roles
Lets you setup the roles in Ignition.

CORE2036
Ignition > Security > Users
Lets you setup the user in Ignition.

CORE2056
Ignition > Security > Active Directory
If you are on a Windows domain with AD, you can hook Ignition into AD. Can be used to import users as well as use the Windows password for authentication.

CORE2038
Ignition > Security > Security Configuration
Same thing as manual security but in a nice, easy to use GUI.

CORE2008
Ignition > System > Business Fields
Lets you maintain field metadata. For every field in the DB, you should have the same field here.

CORE2000
Ignition > System > Business Modules
You need one for your application. A module is essentially a DLL with your custom functionality.

CORE2004
Ignition > System > Business Objects
Bad naming looking back, but this is your screen (ie. user control).

CORE2010
Ignition > System > Business Tables
Lets you maintain the tables in Ignition. For every table in the DB, you should have one here. There is also a task off here letting you create the code that corresponds.

CORE2126
Ignition > System > Languages
Lets you defined multi-lingual support. When you create a language, you must also specify the spell check dictionaries to use (defined on CORE2124). Ignition comes with English, two versions of Maori, Modern French and Spanish. Multi-lingual support is limited (at the moment) to word replacement.

CORE2118
Ignition > System > Relations
This is partially implemented - basically you setup the relations from one table to another. The only place right now this is used is reporting - for the XtraReport designer to figure out relations, you must put them in here.

CORE2124
Ignition > System > SpellChecking
This screen lets you load the spell check dictionaries. You setup the name, put in the core words, the grammar and the user words.

CORE2092
Ignition > System > System Help
Lets you put in your own help. Great for task based help instead of function.

CORE2092.IMAGES
Ignition > System > System Help Images
Lets you upload any images you want to be used in help. The best way to learn how this works is to check out the existing help.

CORE2068
Ignition > System > System History
Lets you defined the system history tables that are used for transaction recording. You can then see all the transactions processed on that table.

CORE2088
Ignition > System > System Scripts
Lets you create scripts that can be used in reporting, data imports/exports and scripts for searching. To see a working example, check out the "Security Search Script". This is used when searching for manual security items.

CORE2094
Ignition > System > System Settings
Lets you see and modify all system settings.

CORE2110
Ignition > System > Control Panel
Configures core functionality. Things like the header color, environment colors, the environment type, if the password should be forced entry, active directory settings, shortcut settings, help settings (like title, version, pre and post processing), email settings, custom logos and images, the product key, and any custom skins used. You can also restrict what skins can be used and you can also set a forced skin for all users.

CORE2122.IMPORTKEY
Ignition > System > Import Product Key
Lets you import the product key. Ignition comes with an app that lets you generate the key.

CORE2046
Ignition > Utilities > Attachments
Lets you browse all attachments in the system.

CORE2060
Ignition > Utilities > Ignition Tips
Lets you modify the tip of the days used.

CORE2030
Ignition > Utilities > Quotations
Lets you modify the system quotes.

CORE2044
Ignition > Utilities > Stickies
Lets you browse all stickies in the system.

CORE2028
Ignition > Utilities > World Events
Lets you modify the system world events.

CORE2026
Ignition > Utilities > Exceptions
Lets you view exceptions that have been raised. This includes all client exceptions and also exceptions from the server apps.

CORE2012
Ignition > Utilities > Record Locks
Lets you see the locks that have been placed on records.

CORE2062
Ignition > Utilities > Statistics
Lets you view how the system is being used.

CORE2012.CLEARALL
Ignition > Utilities > Clear All Locks
Simple routine that will clear all record locks. Great example to see how a function is used in Ignition as opposed to a user entry screen.

CORE2120
Ignition > Web > Fusion Bindings
Lets you maintain Fusion bindings. These aren't used for the normal WinForms user controls, but they must be used if you are going down the WPF road. WPF screens require a webservice for business logic processing. Obviously the Fusion service needs to be working.

CORE2114
Ignition > Web > WebService Access
Lets you setup what webservices will be available and what users can access the webservice. Webservices can also be setup to be available to all authenticated users. For the more secure webservices, you can restrict security to designated users.

Thes screens apply to the ROSTER module.

ROSTER2020
Rostering > Schedule > My Schedule
Read only screen to see what the logged in user has scheduled for work.

ROSTER2000
Rostering > Schedule > Schedule Management
Main screen to modify all users schedules. You can setup security so certain profiles can edit data on this screen, but other users can only view.

ROSTER1140
Rostering > Control Panel > Categories
Lets you maintain categories for schedules.

ROSTER1000
Rostering > Control Panel > Employees
Lets you maintain employees for schedules. The ROSTER module doesn't work off the user table because you may want a custom list. You can however indicate a user as a system user.

ROSTER1160
Rostering > Control Panel > Locations
Lets you maintain locations for schedules.

ROSTER1020
Rostering > Control Panel > Positions
Lets you maintain positions for schedules.

ROSTER1100
Rostering > Control Panel > Shifts
Lets you maintain shifts for schedules.

ROSTER1060
Rostering > Control Panel > Skills
Lets you maintain skills for schedules.

ROSTER1040
Rostering > Control Panel > Teams
Lets you maintain teams for schedules.

Thes screens apply to the MISC module.

MISC2000
Miscellaneous > Developer > Code Snippets
Simple screen that I use to store code snippets.

MISC2020
Miscellaneous > Developer > Expression Tester
I did not create the bulk of this code. I forgot where I got this from, but this code IS NOT mine. I wanted a built in way to Ignition to manipulate regular expressions and this worked out pretty fine. It's also a real complicated screen.

MISC2010
Miscellaneous > Developer > XML Tester
Quite often I work with XML and need to format it - this screen does that. And stores the XML too.

MISC2006
Miscellaneous > Other Tools > Dictionary
Test screen more than anything else to test large records.

MISC2002
Miscellaneous > Other Tools > Journal
Another test screen to test journal entry. Great example of how a search can be used to only return data for the logged in user.

MISC2004
Miscellaneous > Other Tools > WPF Test
Need to have Fusion working to get this screen to work. Simple test of a WPF screen.

The other client app - Plasma

The other client application is called Plasma. There is nothing real special about this app, it just uses the multicasting described below to find all the services on the network and provide diagnostic information about the services.




Plasma seeing four different services in two environments (DEMO, LIVE).

So why would you want to use this tool? It is for admins, it's not for end users. For Reactor, you can see the connected clients, the recent database transactions, the actual database connections and any events the service deems rather important. For Fusion, you can see the tokens that have been requested (both un-authorized and authorized), the functions that have been executed and the services that are hosted. For Scheduler, you can see the running schedules and the schedules that have been marked to run. It could be argued that this information could be put in a screen in Ignition, but I wanted a seperate application just for admin without the entire client.

Cool Feature - Multicasting

In all the application configs (either the .config file or XML settings), there is a key called MulticastURL. All applications either listen on or broadcast on this URL/IP combination. By default it is 224.100.0.1:61555. The server side code looks like:

    Public Class UdpServerApplication
        Public Event OnReceivedRequest(ByVal ClientIP As String)
        Public Event OnError(ByVal e As Exception)

        Private _Port As Integer
        Private _gAddress As String
        Private _ServiceName As String
        Private _ServiceGroup As String
        Private _ServiceFriendly As String
        Private _ServiceURL As String
        Private _ServiceHealthURL As String
        Private _ServiceApplication As String
        Private _ServiceLoad As String
        Private _ServiceOrder As Integer
        Private _ServiceXML As String
        Private _ServerSock As UdpClient = Nothing
        '
        Private _ListenThread As Thread
        Private _IsListening As Boolean = False

        Public Sub New(ByVal ServiceName As String, ByVal ServiceFriendly As String, _
          ByVal ServiceURL As String, ByVal ServiceHealthURL As String, ByVal ServiceGroup As String, _
          ByVal ServiceApplication As String, ByVal ServiceLoad As String, ByVal ServiceOrder As Integer)
            Me.New(String.Empty, 0, ServiceName, ServiceFriendly, ServiceURL, ServiceHealthURL, _
              ServiceGroup, ServiceApplication, ServiceLoad, ServiceOrder)
        End Sub

        Public Sub New(ByVal GroupAddress As String, ByVal Port As Integer, _
          ByVal ServiceName As String, ByVal ServiceFriendly As String, _
          ByVal ServiceURL As String, ByVal ServiceHealthURL As String, ByVal ServiceGroup As String, _
          ByVal ServiceApplication As String, ByVal ServiceLoad As String, ByVal ServiceOrder As Integer)
            ' do some basic checking.
            If (Port <= 0) Then
                Port = 61555
            End If
            If (String.IsNullOrEmpty(GroupAddress)) Then
                GroupAddress = "224.100.0.1"
            End If

            ' set the vars.
            Me._Port = Port
            Me._gAddress = GroupAddress
            Me._ServiceName = SecurityElement.Escape(ServiceName)
            Me._ServiceFriendly = SecurityElement.Escape(ServiceFriendly)
            Me._ServiceURL = SecurityElement.Escape(ServiceURL)
            Me._ServiceHealthURL = SecurityElement.Escape(ServiceHealthURL)
            Me._ServiceGroup = SecurityElement.Escape(ServiceGroup)
            Me._ServiceApplication = SecurityElement.Escape(ServiceApplication)
            Me._ServiceLoad = SecurityElement.Escape(ServiceLoad)
            Me._ServiceOrder = ServiceOrder

            ' build up the service info just this once.
            ' this xml is sent back to clients wanting to know what the service is about.
            Me._ServiceXML = BuildServiceInfo()
        End Sub

        Public Sub Start()
            If (Me._IsListening = False) Then
                Me._IsListening = True
                Me._ListenThread = New Thread(AddressOf StartListener)
                Me._ListenThread.Start()
            End If
        End Sub

        Public Sub [Stop]()
            Me._IsListening = False
            Try
                Me._ServerSock.Close()
            Catch ex As Exception
            End Try
        End Sub

        Private Sub StartListener()
            Dim groupEP As IPEndPoint
            Dim groupAddress As IPAddress
            '
            Try
                Me._ServerSock = New UdpClient
                groupAddress = IPAddress.Parse(_gAddress)
                groupEP = New IPEndPoint(IPAddress.Any, _Port)
                '
                Me._ServerSock.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True)
                Me._ServerSock.Client.Bind(groupEP)
                Me._ServerSock.JoinMulticastGroup(groupAddress)

                ' keep listening until we're told to stop.
                While (Me._IsListening)
                    Dim bytes As Byte() = Nothing
                    Dim response As Byte() = Nothing
                    Dim buffer As String = String.Empty
                    Dim message As UdpMessage
                    Dim sendResponse As Boolean = False
                    '
                    Try
                        bytes = _ServerSock.Receive(groupEP)
                        buffer = Encoding.Unicode.GetString(bytes, 0, bytes.Length)
                        RaiseEvent OnReceivedRequest(groupEP.ToString())

                        ' parse the message into an object.
                        Try
                            message = New UdpMessage(buffer)
                        Catch ex As Exception
                            message = Nothing
                        End Try

                        ' do something based on the type.
                        If (message IsNot Nothing) Then
                            ' see if we have a filter.
                            If (String.IsNullOrEmpty(message.MessageFilter)) Then
                                ' no filter at all, let message through.
                                sendResponse = True
                            Else
                                ' if we have a filter and the app name is not in there,
                                ' do not allow a send. example, filter is "REACTOR,FUSION".
                                ' in this case we only want responses from reactor or fusion
                                ' but not scheduler or firestorm.
                                If (message.MessageFilter.ToLower.Contains(Me._ServiceApplication.ToLower)) Then
                                    sendResponse = True
                                Else
                                    sendResponse = False
                                End If
                            End If

                            Select Case message.MessageType.ToLower
                                Case "service.info"
                                    response = System.Text.Encoding.Unicode.GetBytes(Me._ServiceXML)
                            End Select

                            ' send the response back to the client.
                            If (sendResponse) Then
                                If (response IsNot Nothing) Then
                                    Me._ServerSock.Send(response, response.Length, groupEP)
                                End If
                            End If
                        End If

                    Catch ex As Exception
                    End Try
                End While

            Catch ex As Exception
                RaiseEvent OnError(ex)
            Finally
                If (Me._ServerSock IsNot Nothing) Then
                    Me._ServerSock.Close()
                End If
            End Try
        End Sub

        Private Function BuildServiceInfo() As String
            Dim xBuilder As New StringBuilder
            '
            xBuilder.AppendLine("<viperXml type=""service.info.response"" version=""1.0"">")
            xBuilder.AppendFormat("  <service name=""{0}"" friendly=""{1}"" url=""{2}"" 
              healthUrl=""{3}"" group=""{4}"" application=""{5}"" load=""{6}"" order=""{7}""/>", _
                Me._ServiceName, _
                Me._ServiceFriendly, _
                Me._ServiceURL, _
                Me._ServiceHealthURL, _
                Me._ServiceGroup, _
                Me._ServiceApplication, _
                Me._ServiceLoad, _
                Me._ServiceOrder _
                )
            xBuilder.AppendLine("</viperXml>")
            '
            Return xBuilder.ToString
        End Function
    End Class
Then you have the client code:
    Public Class UdpClientApplication
        Public Event OnServiceFound(ByVal FoundService As ServiceItem)
        Public Event OnError(ByVal e As Exception)

        Private _Port As Integer
        Private _gAddress As String
        Private _ServiceMessage As UdpMessage
        Private _ClientSock As UdpClient

        Private _AppFilter As String
        Private _SendThread As Thread
        Private _SendTime As Integer
        Private _IsSending As Boolean = False

        Public Sub New(ByVal Filter As String)
            Me.New(String.Empty, 0, 2, Filter)
        End Sub

        Public Sub New(ByVal GroupAddress As String, ByVal Port As Integer, ByVal Timeout As Integer, ByVal Filter As String)
            ' do some basic checking.
            If (Port <= 0) Then
                Port = 61555
            End If
            If (String.IsNullOrEmpty(GroupAddress)) Then
                GroupAddress = "224.100.0.1"
            End If
            ' only let timeouts occur from 1 second to 30 seconds.
            ' this is how long the client app looks for servers.
            If (Timeout <= 0) Then
                Timeout = 10
            ElseIf (Timeout > 30) Then
                Timeout = 30
            End If
            Me._SendTime = Timeout
            Me._AppFilter = Filter

            ' set the vars.
            Me._Port = Port
            Me._gAddress = GroupAddress

            ' create a service message.
            Me._ServiceMessage = New UdpMessage
            Me._ServiceMessage.MessageType = "service.info"
            Me._ServiceMessage.MessageFilter = Me._AppFilter
        End Sub

        Public Sub Start()
            If (Me._IsSending = False) Then
                Me._IsSending = True
                Me._SendThread = New Thread(AddressOf StartSender)
                Me._SendThread.Start()
            End If
        End Sub

        Public Sub [Stop]()
            Me._IsSending = False
            Try
                _ClientSock.Close()
            Catch ex As Exception
            End Try
        End Sub

        Private Sub StartSender()
            Dim iep As IPEndPoint
            Dim sendMsg As Byte()
            Dim count As Integer
            '
            Try
                _ClientSock = New UdpClient
                _ClientSock.EnableBroadcast = True
                iep = New IPEndPoint(IPAddress.Parse(Me._gAddress), Me._Port)
                sendMsg = Encoding.Unicode.GetBytes(Me._ServiceMessage.ToXML)

                Dim s As New UdpState
                s.Client = _ClientSock
                s.EndPoint = iep

                count = _SendTime * 16
                For i As Integer = 0 To count
                    If (Me._IsSending = False) Then
                        Exit For
                    Else
                        ' send the message, and process the receive in the callback.
                        _ClientSock.Send(sendMsg, sendMsg.Length, iep)
                        _ClientSock.BeginReceive(New AsyncCallback(AddressOf ReceiveCalback), s)
                        '
                        Thread.Sleep(40)
                    End If
                Next

            Catch ex As Exception
                RaiseEvent OnError(ex)
            Finally
                _ClientSock.Close()
            End Try
        End Sub

        Private Sub ReceiveCalback(ByVal ar As IAsyncResult)
            Dim myEnv As ServiceItem
            Dim uClient As UdpClient
            Dim ePoint As IPEndPoint
            Dim receiveBytes() As Byte
            Dim receiveString As String
            '
            Try
                ' get the client and endpoint, process the receieve
                ' and then pullback the XML.
                uClient = CType((CType(ar.AsyncState, UdpState)).Client, UdpClient)
                ePoint = CType((CType(ar.AsyncState, UdpState)).EndPoint, IPEndPoint)
                receiveBytes = uClient.EndReceive(ar, ePoint)
                receiveString = Encoding.Unicode.GetString(receiveBytes)

                ' get the environment.
                myEnv = GetService(receiveString)
                If (myEnv IsNot Nothing) Then
                    RaiseEvent OnServiceFound(myEnv)
                End If

            Catch ex As Exception
                ' ignore this, doesn't really matter.
            End Try
        End Sub

        Private Function GetService(ByVal XMLString As String) As ServiceItem
            Dim xDoc As Xml.XmlDocument
            Dim svcNode As Xml.XmlNode
            Dim env As ServiceItem = Nothing
            '
            Try
                xDoc = New Xml.XmlDocument
                xDoc.LoadXml(XMLString)
                '
                svcNode = xDoc.SelectSingleNode("/viperXml/service")
                If (svcNode IsNot Nothing) Then
                    env = New ServiceItem
                    env.ServiceName = svcNode.Attributes("name").Value
                    env.ServiceFriendly = svcNode.Attributes("friendly").Value
                    env.ServiceURL = svcNode.Attributes("url").Value
                    env.ServiceHealthURL = svcNode.Attributes("healthUrl").Value
                    env.ServiceGroup = svcNode.Attributes("group").Value
                    env.ServiceApplication = svcNode.Attributes("application").Value
                    env.ServiceLoad = svcNode.Attributes("load").Value
                    '
                    Try
                        env.ServiceOrder = CInt(svcNode.Attributes("order").Value)
                    Catch ex As Exception
                        env.ServiceOrder = 0
                    End Try
                End If

                ' return.
                Return env

            Catch ex As Exception
                Return Nothing
            End Try
        End Function

        Private Class UdpState
            Private _EndPoint As IPEndPoint
            Private _Client As UdpClient

            Public Property EndPoint() As IPEndPoint
                Get
                    Return Me._EndPoint
                End Get
                Set(ByVal value As IPEndPoint)
                    Me._EndPoint = value
                End Set
            End Property

            Public Property Client() As UdpClient
                Get
                    Return Me._Client
                End Get
                Set(ByVal value As UdpClient)
                    Me._Client = value
                End Set
            End Property
        End Class
    End Class
By using this multicasting, the client application can get information about the server application without the need for any config files (well, apart from the server side configs). The idea was to get the client application as thin as possible so that the client doesn't know anything about the installation. Just install and run. The code above was developed from a variety of sources. So by using this multicasting, you can just create another environment (like DEV or LIVE) and the client will just know about it, no reconfiguration of configs required.

The multicasting implemented works like this:
1 - You have server applications (Reactor, Fusion, Scheduler) listening for requests.
2 - A client application broadcasts on an IP/Port, requesting info from any services.
3 - The services respond back with connection information.
4 - The client then connects.

There is also filtering involved. The Ignition WinForms application only needs to know about Reactor services. So when it sends its message, it says, I only want to know about Reactor applications. Although all server apps get the request, only the Reactor application sends a response back. I've tested this on single computer setups, on small networks and on large networks. Works a charm in all tested scenarios. Where it falls over is when you connect to a VPN - I've noticed in some cases, the client doesn't get any responses back. If you have the server app on a computer with a VPN, it really doesn't like that, but that's a very small chance a server would be on a VPN.

There is actually a seperate project, not in the Ignition realm that demonstrates how you could implement this. Parts of it are designed for Ignition, but it's totally standalone. Check out the ViperWorks.Multicast project in the source to see how you could implement something like this.

The Reactor Application

Very early in the development of Ignition, the Reactor server-side application was developed. This application exists solely to perform database transactions. Sadly, the WinForms client connects straight into this service making the framework pretty fat. If I were to redesign Ignition, I wouldn't have the client connect into Reactor, but connect to Fusion (webservice hosting) instead. It's not entirely a bad thing, the system works, I would just go down a different road if I were to do this again.

Basically when a transaction comes through (like an Insert), Reactor does this:

    Public Function DataInsert(ByVal ClientID As System.Guid, _
      ByVal FieldList As List(Of BusinessField), ByVal TableName As String, _
      ByVal KeyColumns() As String) As String() Implements IReactorContract.DataInsert
        Dim Client As ReactorClient = Nothing
        Dim ReturnData As String()
        Dim SQLCommand As StringBuilder = Nothing
        Dim StartTime As Date = Now
        Dim conn As ServerConnection = Nothing
        Dim cCommand As String = String.Empty
        '
        Try
            cCommand = String.Format("INSERT ({0})", TableName)
            Client = FindClientItem(ClientID)
            '
            If (Client IsNot Nothing) Then
                ' check to see if we should log commands. if we are create an
                ' instance of a stringbuilder. if stringbuilder is nothing when passed into
                ' the databaselogic (GenericData), it will not be set.
                If (ReactorHelper.SQLCommandHistoryMax > 0) Then
                    SQLCommand = New StringBuilder
                End If

                ' get a connection.
                conn = ReactorHelper.CheckoutConnection(Client, cCommand)
                ReactorHelper.IncrementRequests()
                StartTime = Now
                Client.TimeLastAccess = Now
                Client.RequestCurrent = cCommand
                Client.ClientStatus = ReactorClient.eClientStatus.Busy

                ' run the command, log the command and add the SQL command text to history.
                ReturnData = conn.Connection.Insert(FieldList, TableName, KeyColumns, SQLCommand)

                ' put the data into the logs.
                Core.ReactorHelper.AddInsertTranaction(TableName, ReturnData, Client.ClientUser)
                Core.ReactorHelper.LogCommand(Client, "INSERT", TableName, StartTime, Now)
                Me.AddSQLCommand(SQLCommand, Client, StartTime, Now, True)
                '
                Return ReturnData
            Else
                Return Nothing
            End If

        Catch ex As Exception
            Dim reactorEx As New ReactorException
            Dim infoString As String
            '
            If (TypeOf ex Is Exceptions.DataTableLockException) Then
                infoString = String.Format(String.Format( _
                  "TABLE LOCKED: The table {0} is locked from write access. Locked on {1} by {2}.", _
                    CType(ex, Exceptions.DataTableLockException).TableName, _
                    CType(ex, Exceptions.DataTableLockException).LockDate.ToString, _
                    CType(ex, Exceptions.DataTableLockException).LockUser _
                ))
            Else
                infoString = "INSERT FAILURE: DataInsert"
            End If

            ' log the exception, and add the command as a failed SQL command.
            Core.ReactorHelper.LogException(Client, ex)
            Me.AddSQLCommand(SQLCommand, Client, StartTime, Now, False)
            '
            If (SQLCommand IsNot Nothing) Then
                reactorEx = New ReactorException( _
                    ex.Source, _
                    ex.StackTrace, _
                    ex.Message, _
                    ex.ToString, _
                    SQLCommand.ToString _
                )
            Else
                reactorEx = New ReactorException( _
                    ex.Source, _
                    ex.StackTrace, _
                    ex.Message, _
                    ex.ToString, _
                    String.Empty _
                )
            End If
            '
            Throw New FaultException(Of ReactorException)(reactorEx, infoString)
            Return Nothing

        Finally
            ' check the connection back in for use and set client info.
            If (conn IsNot Nothing) Then
                ReactorHelper.CheckInConnection(Client, conn)
            End If
            If (Client IsNot Nothing) Then
                ReactorHelper.DecrementRequests()
                Client.TimeLastAccess = Now
                Client.RequestLast = String.Format("INSERT ({0})", TableName)
                Client.RequestCurrent = ""
                Client.ClientStatus = ReactorClient.eClientStatus.Idle
            End If
        End Try
    End Function
So first off, Reactor finds the client based on the passed in ClientID. It then checks out an actual DB connection. This connection is essentially an instance of IDBInterface (see ViperWorks.Core.Data.IDBInterface). We then increment the request count, set statistic data on the client, and then perform the Insert on the connection. After that we log the request and then return the data. In the Finally statement, we check the connection back in. For those of you that downloaded the source to Ignition a couple years back, Reactor didn't have this check in/out functionality. As a result of this, Reactor wasn't very thread friendly and caused a bit of chaos in moderate load situations.

I won't go into the entire structure of Reactor here. If there is demand, I'll write another article devoted to Reactor and how the application works in whole. To keep moving along, we'll take a quick look at the config of Reactor.

<configuration>
  <application>
    <!-- ServiceName is the name to run the service as. This name is used as the Windows Service name, -->
    <!-- the service URI is what is used to indicate the contract name. -->
    <setting key="ServiceName" value="ViperWorks.Ignition.Reactor.DEMO"/>
    <setting key="ServiceDescription" value="Demo Database Service for ViperWorks Reactor."/>
    <setting key="ServicePort" value="8810"/>
    <setting key="ServiceURI" value="demo.console"/>

    <!-- service information -->
    <setting key="ApplicationName" value="DEMO.CLIENT.CONSOLE"/>
    <setting key="ApplicationGroup" value="DEMO"/>
    <setting key="ApplicationFriendly" value="DEMO (Default)"/>
    <setting key="ApplicationLoad" value="SERVER,CLIENT"/>
    <setting key="ApplicationOrder" value="0"/>
    
    <!-- healthport and uri is the port and uri to listen for health requests..-->
    <setting key="HealthPort" value="8000"/>
    <setting key="HealthURI" value="demo.console"/>
    <setting key="MulticastURL" value="224.100.0.1:61555"/>
    
    <!-- DataAssembly is the namespace of the data assembly. -->
    <!-- Set database to the file location if using Access. -->
    <setting key="DataAssembly" value="ViperWorks.Core.Data"/>
    <setting key="DataAssemblyType" value="ViperWorks.Core.Data.Access.DatabaseInterface"/>
    <setting key="DataBase" value="\Data\ViperWorks.Ignition.Reactor.Access.mdb"/>
    <setting key="DataServer" value="localhost"/>
    <setting key="DataUsername" value=""/>
    <setting key="DataPassword" value=""/>
    
    <!-- Controls the logging activity. Directory is where log files are kept. -->
    <!-- LogType's default value is: COMMAND,INFO,EXCEPTION,SQL,FILE,EVENT. -->
    <!-- COMMAND   - log all commands (eg. INSERT, UDPATE, etc) -->
    <!-- INFO      - log information messages (eg. SENDMAIL, CONNECTIONOPEN, etc) -->
    <!-- EXCEPTION - log exceptions -->
    <!-- SQL       - log SQL commands to a file (database specific) -->
    <!-- FILE      - log messages to a file -->
    <!-- EVENT     - log messages to the event log -->

    <!-- SQL commands are only written to file, not event log. -->
    <!-- If LogDirectory is blank, logs are written to temp dir. -->
    <!-- If LogDirectory is \, logs are written to app dir. -->
    <!-- Value such as \Log is permitted. -->
    <setting key="LogDirectory" value="\Logs"/>
    <setting key="LogType" value="EXCEPTION,SQL,FILE"/>

    <!-- The temporary directory. -->
    <!-- If TempDirectory is \, logs are written to app dir. -->
    <!-- Value such as \Temp is permitted. -->
    <setting key="TempDirectory" value="\Temp"/>

    <!-- The amount of SQL commands performed to store in cache. -->
    <setting key="CommandHistoryMax" value="200"/>

    <!-- The names of the DB tables. -->
    <setting key="BusinessTableName" value="XSYS_BUSINESSTABLE"/>
    <setting key="BusinessFieldName" value="XSYS_BUSINESSFIELD"/>
    <setting key="TransactionName" value="XSYS_TRANSACTION"/>
    <setting key="TransactionEnabled" value="false"/>

    <!-- The refresh interval for metadata (in minutes). -->
    <!-- Set to -1 for a once off. -->
    <!-- Set to 0 to disable refresh. -->
    <!-- Use lower number (faster) for development. -->
    <!-- Use higher number (slower) for production. -->
    <setting key="MetaRefreshInterval" value="1"/>    
  </application>
</configuration>

Service
The service part of the config configures what core service information. The service name and description is what is showed in the Windows Service window. The Port is the port Reactor listens on for WCF connections and same thing with the URI.

Application
The application part is used in an important part, the multicasting. The application name is the unique name for the service. Application group is the group of applications. In a real world scenario, this would be DEV, TEST, or LIVE. Friendly is just the user friendly name. Load is the type of service. Sometimes you want to create multiple Reactors for load balancing. The Ignition client will only display CLIENT load types not SERVER. So if you want a reactor for server use, only put SERVER in there, and the Ignition client won't see it. Order is the order this environment should be displayed in.

Health
Each service (Reactor, Fusion, Scheduler) hosts a generic WCF contract (in the case of Reactor and Fusion, this is in addition to the already hosted contracts) that contains health information. The Plasma tool (see above) uses this information to provide diagnostic information to system admins.

Data
Reactor needs to know how to connect to the database. That's what this section is for. The DataAssembly is the name of the assembly which contains your driver. For Access, MSSQL and MySQL connections this will be ViperWorks.Core.Data. If you create your own assembly, put this in here. DataAssemblyType is the name of the class (include the entire name, not just the class name). In the supplied config this is ViperWorks.Core.Data.Access.DatabaseInterface. Change this to ViperWorks.Core.Data.MSSQL.DatabaseInterface for the MSSQL driver or ViperWorks.Core.Data.MySQL.DatabaseInterface for the MySQL driver. Database is the name of the database, for MDB, this is the file path. Put a slash at the front to use the current directory as the start. Server is the name of the DB server (use something like SERVER\INSTANCE for instance explicit configuration). And Username/Password is the usual stuff. From memory, if you don't put this in here for MSSQL implementation, integrated security will be used.

Logging
Logging is pretty self-explanatory, see the XML comments for more info.

Command History
This history can be used in Plasma. Ideally, don't keep this too large in production scenarios. You can use this to see all the recent transactions being pushed through Reactor. If you want a full history, use the logging above and write out SQL commands to a text file. Be warned this will affect performance. It's not too bad of a hit, but it is noticeable in MDB scenarios.

DB Tables, Transaction and MetaRefresh
Although not fully implemented, Ignition can maintain a list of metadata relating to Tables and Fields. You specify in here what the names of the tables are. BusinessTableName is used for pulling back all the tables in the system. This was going to be used for the notification - some tables you want to see notifications for, so the service needs to know about them. And Fields was going to be used to enforce your rules. So if a field is setup to be upper case, Reactor would up case the data. TransactionName is the name of the transaction table - used for notifications. I've set the included release to not use transactions. MetaRefreshInterval is how often Reactor should update its metadata. Use -1 for a once off when the service starts, 0 to disable or any other number for the refresh interval in minutes.

Another cool feature of the server apps is that they use port sharing. This enables me or you to have all the services using standard ports, the only thing that changes it the URI. Great if you have multiple services on a site since you don't have to have a different port for each service.

Make sense? Just remember that Reactor is to serve up database requests. It sits over what ever database you are using and puts a object layer over the top. The Ignition client then uses the business logic, the business logic connects into Reactor and does its work.

The Fusion Application

Fusion came after the WinForms client was written (well, half written). Looking back, this should have been written after Reactor. Fusion basically sits on top of Reactor. Based on the configuration you did via the WinForms client, Fusion will host many different webservices that exposes your business logic via WCF contracts. It's really that simple. It takes apart your code via Reflection, and puts a wrapper over top of it. So if you had a function called GetLollipops and that returned a String, it would be turned into GetLollipops(Token As String). Every method you expose externally requires a token. If an invalid token is specified, you can't use that method.

The token you need can be obtained from the authentication service (so http://localhost/authentication). Basically you pass in your username and your web password (web password is on XSYS_USER table), the webservice ID and you can get your token. Tokens expire after a set amount of time.

I've done some testing with using SSL and Fusion and yes - you can do it. It's a bit of a pain, mainly in the Windows side of things, but it is possible so you can protect the communication channels. As a downside though, you can't host Fusion within IIS7 or above. Fusion was designed as a standalone application to host webservices.

We'll take a quick look at the config, it's similar to Reactor, but also different:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <application>
    <!-- ServiceName is the name to run the service as. This name is used as the Windows Service name. -->
    <!-- HttpPort is the HTTP port to listen - same goes with NetTcpPort. -->
    <!-- Hostname is the host name to listen on - in cases of local hosting, put server name. -->
    <!-- In cases of web hosting, put the domain name (ie. ignition.harfordmedia.com) -->
    <setting key="ServiceName" value="ViperWorks.Ignition.Fusion.DEMO"/>
    <setting key="ServiceDescription" value="Fusion Service for DEMO."/>
    <setting key="HttpPort" value="80"/>
    <setting key="NetTcpPort" value="8127"/>
    <setting key="Hostname" value=""/>

    <setting key="TokenTimeOut" value="20"/>
    <setting key="TokenRemoval" value="60"/>

    <!-- SecureMode is if SSL security should take place. SSL certs need to be done outside Fusion. -->
    <!-- SecureCertificate is the file location of the PFX (only used for nettcp comms). -->
    <!-- SecureCertificatePassword is the password for the PFX (leave blank for none). -->
    <!-- ForceBasicBindings can be set to True to force simple services (for non .NET integration). -->
    <!-- Note that basic bindings will not work for Ignition applications. -->
    <setting key="SecureMode" value="false"/>
    <setting key="SecureCertificate" value="c:\mycert.pfx"/>
    <setting key="SecureCertificatePassword" value="test"/>
    <setting key="ForceBasicBindings" value="false"/>

    <!-- healthport & uri is the port & uri to listen for health requests..-->
    <setting key="HealthPort" value="8888"/>
    <setting key="HealthURI" value="fusion"/>

    <!-- service information -->
    <setting key="ApplicationName" value="FUSION_DEMO"/>
    <setting key="ApplicationGroup" value="DEMO"/>
    <setting key="ApplicationFriendly" value="Demo Fusion"/>
    <setting key="ApplicationLoad" value="SERVER"/>
    <setting key="ApplicationOrder" value="0"/>
    
    <!-- reactor url is the url of reactor to connect to -->
    <!-- multicasturl is the url to use for multicasting -->
    <setting key="ReactorURL" value="net.tcp://localhost:8810/demo.console"/>
    <setting key="MulticastURL" value="224.100.0.1:61555"/>
    
    <!-- Controls the logging activity. Directory is where log files are kept. -->
    <!-- LogType's default value is: INFO,EXCEPTION,FILE,EVENT. -->
    <!-- INFO      - log information messages (eg. SENDMAIL, CONNECTIONOPEN, etc) -->
    <!-- EXCEPTION - log exceptions -->
    <!-- FILE      - log messages to a file -->
    <!-- EVENT     - log messages to the event log -->
    <setting key="LogDirectory" value="\Logs"/>
    <setting key="LogType" value="INFO,EXCEPTION,EVENT,FILE"/>

    <!-- specify a semicolon seperated list of services this service depends on (end with semicolon) -->
    <setting key="DependantOnServices" value = "ViperWorks.Ignition.Reactor.DEMO;" />
  </application>
</configuration>

Service
The service part of the config configures what core service information. The service name and description is what is showed in the Windows Service window. The HTTP port is the port that the http WCF contract is bound to. Same story with the NetTcpPort. Set to 0 to not use the specified contract type. Host name is the name of the host the bindings will use. This isn't always the name of the server. Say in a situation where you are exposing the webservice externally. You might have a hostname like http://ignition.company.com. In this case, the host name will be ignition.company.com. TokenTimeOut is when a token will be marked as inactive and not available for further use. TokenRemoval is when all traces of that token will be removed from memory.

If you change the ports here, you must change them in the binding screen (see CORE2120 above). If you don't change them in that screen, the Ignition client will not know what ports to use for communication when creating an instance of the screen. By default, the bindings are setup for localhost. Change that in a proper networking scenario.

Security
SecureMode is if Fusion should be using SSL security. The related bindings will then be changed. SecureCertificate is the location of the PFX file (read up on SSL security if you don't know what this is). And the password, well, that's the password for the PFX file. ForceBasicBindings is for situations where you need to provide integration for non .NET applications. For example, you can have one Fusion service serving up .NET apps, but you can have another for serving other types of applications, perhaps in data integration scenarios. HealthPort and HealthURI is the same story as Reactor. The port can stay the same, but the URI must be unique for every service.

Application
The application part is used in an important part, the multicasting. The application name is the unique name for the service. Application group is the group of applications. In a real world scenario, this would be DEV, TEST, or LIVE. Friendly is just the user friendly name. Load is the type of service. Sometimes you want to create multiple Fusions for load balancing. The Ignition client will only display CLIENT load types not SERVER. So if you want a reactor for server use, only put SERVER in there, and the Ignition client won't see it. The Ignition WinForms client won't actually see the Fusion service (it filters these services out), but it's in there for any future use. Order is the order this environment should be displayed in.

URL's and Logging
The Reactor URL is the URL of reactor that Fusion should connect to. The multicast URL is used when multicasting. If you change this, it is advised to change it for every single application. Logging is the same concept as Reactor. DependantOnServices is only used when using Fusion as a service - when installing the service, the services listed here will be the services Fusion is dependent upon (so if you stop Reactor, Fusion will stop also).

A deeper look
We're going to take a deeper look into Fusion now. Mainly because SOA is a good thing. Some enterprises don't even have a good one, some have them but are super complicated. Fusion is somewhere in between. It's very customizeable, has tight security and lets you get at your prized business logic via webservices. We'll take a look at basic bindings for this example. Just so we can use the included WebServiceStudio (only included so you don't have to go hunting if you don't have it) to test this out. You could also use the supplied tools in VS2008+ (WcfTestClient.exe in the VS directory \Common7\IDE). So if you haven't already, start Reactor, then start the Console app in the Fusion directory (remember, the .Basic one). Replace localhost if you need to in the URLs below. Before we get into it, each Fusion service will host an authentication webservice at the root. The reason for this is that to perform any transaction on webservices, you need to authenticate yourself. The authentication webservice provides a standard, generic way for your application to authenticate.

All going well, you have started Reactor and Fusion. Next thing is to open up WebServiceStudio. Then:
  • Enter this URL: http://localhost:8830/authentication
  • Choose the GetTokenMethod
  • Enter ADMIN for username and WEB for password.
  • Enter 000EABCF34AF402A865B695E0A7E2A56 as the WebserviceID (you can find this using Plasma).
  • Hit Invoke. You should have a token returned back to you.
  • Enter this URL: http://localhost:8830/demo/dict/dictionarybus?wsdl
  • Choose the RandomWord method.
  • Enter your token.
  • You should now have a random word back.

That's the Fusion service explained pretty broadly, doesn't get much more complicated than that. Note that tokens do expire, so you'll need to handle this in your applications. The majority of the testing done was on a Windows 7 machine. I have done other testing with Windows XP and Windows 2003 (with all SP's) and have noticed that Windows XP and WebServiceStudio can behave oddly. If you run into problems with Fusion (with basic bindings) try using the WcfTestClient application in Visual Studio.

The Scheduler Application

Scheduler solely exists to run scheduled tasks. One example would be to run a script that performs data integration. I won't go into much of this here, but the config file is nearly the same as Fusion, with the exception of many removed keys and one added key called FromEmail - this is the email address emails are sent from for Scheduler progress.

Key Generation

In the Utilities directory, you'll find an application called ViperWorks.Core.KeyGenerator. You can use this tool to generate product keys. By default Ignition comes with a key that expires in 2010 and is registered to ViperWorks Research Limited. Hopefully this shows you one possible way of using product keys. Obviously in a commercial scenario you wouldn't distribute the generator. To give you an idea how it works, here's the code:

    Public Class ProductKeyHelper
        Public Shared Function GenerateProductKey(ByVal ProductName As String, ByVal CompanyName As String, _
           ByVal ExpirationDate As Date, ByVal KeyType As ProductKey.ProductKeyType, _
           ByVal MajorVersion As Integer, ByVal PrivateKey As String, _
           Optional ByVal ConnectionLimit As Integer = 0) As ProductKey
           
            Dim KeyFile As ProductKey
            Dim Buffer As System.Text.StringBuilder
            '
            Try
                ' create the key and the public key.
                KeyFile = New ProductKey
                Dim generator As New Random
                Dim randomValue As Integer
                '
                randomValue = generator.Next(8, 20)
                For i As Integer = 0 To randomValue
                    KeyFile.PublicKey += Guid.NewGuid.ToString("N").ToUpper
                Next

                ' set the properties of the product key.
                KeyFile.ProductName = ProductName
                KeyFile.CompanyName = CompanyName
                KeyFile.ExpirationDate = ExpirationDate
                KeyFile.ConnectionLimit = ConnectionLimit
                KeyFile.KeyType = KeyType
                KeyFile.MajorSoftware = MajorVersion
                '
                Try
                    ' create the serial.
                    Buffer = New System.Text.StringBuilder
                    Buffer.Append(ProductName)
                    Buffer.Append("ý")
                    Buffer.Append(CompanyName)
                    Buffer.Append("ý")
                    Buffer.Append(ExpirationDate.ToString("ddMMyyyy"))
                    Buffer.Append("ý")
                    Buffer.Append(ConnectionLimit)
                    Buffer.Append("ý")
                    Buffer.Append(CInt(KeyType))
                    Buffer.Append("ý")
                    Buffer.Append(CInt(MajorVersion))
                    Buffer.Append("ý")
                    Buffer.Append(KeyFile.PublicKey)
                    Buffer.Append("ý")

                    ' encrypt the data, store as the product key.
                    Buffer.Append(GetSerialNumber(Buffer.ToString))
                    Buffer.Append("ý")
                    KeyFile.ProductKeyValue = Encrypt(Buffer.ToString, KeyFile.PublicKey, PrivateKey)
                    Buffer.Append(KeyFile.ProductKeyValue)
                    '
                    KeyFile.SerialNumber = GetSerialNumber(Buffer.ToString)
                    '
                    Return KeyFile

                Catch ex As Exception
                    Throw New Exception("Could not generate key because the product or company name is too large.", ex)
                End Try

            Catch ex As Exception
                Throw
            End Try
        End Function

        Private Shared Function Encrypt(ByVal PlainText As String, ByVal Key As String, ByVal SubKey As String) As String
            Dim CipherProvider As Encryption.Symmetric.Provider
            Dim CipherSymmetric As Encryption.Symmetric
            Dim CipherData As Encryption.Data
            '
            Try
                CipherProvider = Encryption.Symmetric.Provider.Rijndael
                CipherSymmetric = New Encryption.Symmetric(CipherProvider)
                CipherSymmetric.Key.Text = Key & SubKey
                '
                CipherData = CipherSymmetric.Encrypt(New Encryption.Data(PlainText & SubKey))
                Return CipherData.ToHex

            Catch ex As Exception
                Return String.Empty
            End Try
        End Function

        Private Shared Function GetSerialNumber(ByVal HashData As String) As String
            Dim Buffer As StringBuilder
            Dim Crypto As SHA512CryptoServiceProvider
            Dim ByteData As Byte()
            Dim SingleChar As Integer
            Dim ValidChars As String = "0123456789ABCDEFGHJKLMNPQRTUVWXY"
            '
            Try
                ' compute the hash.
                Buffer = New StringBuilder
                Crypto = New SHA512CryptoServiceProvider
                ByteData = Crypto.ComputeHash(System.Text.Encoding.ASCII.GetBytes(HashData))

                ' make sure no byte data is zero.
                For i As Integer = 0 To ByteData.Length - 1
                    If (ByteData(i) = 0) Then
                        ByteData(i) = 1
                    End If
                Next

                ' create the key.
                For i As Integer = 0 To ByteData.Length - 1
                    SingleChar = ByteData(i) Mod ValidChars.Length
                    Buffer.Append(Mid(ValidChars, SingleChar + 1, 1))
                    '
                    Dim pos As Integer = i + 1
                    If (pos Mod 4 = 0) Then
                        If (i <> ByteData.Length - 1) Then
                            Buffer.Append("-")
                        End If
                    End If
                Next
                '
                Return Buffer.ToString

            Catch ex As Exception
                Throw
                Return ""
            End Try
        End Function
    End Class
IBM UniVerse users (now Rocket) will see a cool little character in there! Yes, this is what the ultimate goal of five years of work. Put a character in the key generation that comes from a 70's database system. Obviously some sarcastic tones in there. Somewhere. Observant programmers will realise that the supplied code doesn't use SHA512CryptoServiceProvider. The reason for this is because this isn't supported (to my knowledge on Windows XP systems). So I've used a different provider in the release code.

Cool feature - Paged Searches

A feature put in quite late was the ability to page search results. This is very common in the web world, but you don't see it often in the WinForms world. Basically when I search in Ignition, most searches come back with the full results. Sometimes its big. Like when you search for World Events, you get 16000+ records back. Quotes is even more. I have done load testing on 200k records, and that took a while to come back. So paging was put in. Basically a special command is executed on the ReactorContract which will bring back paged results.



Here's a screenshot of a paged search. Note the top right - has navigation buttons and a drop down list of the page.

Next up, we'll take a look at a search that isn't paged:



Here's a screenshot of a standard search - no paging available.

To enable paging, you really need to have a suitable table and have something indexed on there. So take a table that stores contacts - you probably have an index on there for the surname. You'd set that up in Ignition on Search Definitions (screen CORE2014). You'd setup a page size (like 5000 records per page) and the field you want paged. Most likely the surname.

Different database systems page in different ways. MySQL is the easiest. You basically put in LIMIT with the start position and the amount of records you want to bring back. Nice and easy. MSSQL you need to use ROW_NUMBER(). Access, well, Access works weird. Paged searches is asking a lot of Access, but it does work. Access you do an inner select, an outer select and then join the two. Sort of beyond the scope of this article, but post a question if you want to know more about how this works.

Cool Feature - Multi-Lingual Support

Another feature that was put in quite late. To give you an idea how fast Ignition can help you develop, Ignition from the ground up was coded for the English language. It was only when I had a possible buyer for Ignition that I thought this through. From start to finish, to enable Ignition for multi-lingual support, it took a full day. If I didn't have the framework, this would have taken much longer. I didn't have to worry about TSQL or anything like that, I just created the tables, envisioned the business processes and got to work. At the end of the day, Ignition was multi-lingual. I'll admit it's not the best way, it's basically word for word replacement with order of precedence.

Other regional settings such as currency and time are not taken into account at this point.

Cool Feature - QuickSearch

This was added quite late in the development of Ignition. Mainly because Ignition is pretty powerful for searching but what if you had a couple screens you went to all the time? And you knew the ID's? So if you had a contacts system, you probably would have a unique ID for the contact (say, autonumber). You just wanted to enter that number in and go straight to the screen. QuickSearch lets you do that. We'll go through a couple examples, first one being searching for a word.

First off, start Ignition. Then enter test in the quick search box, click on the drop down and choose Dictionary. Click on the magnifying glass. You'll go straight to the screen and the record will load. Go back into the box and put in up. The record will change instantly.

Using that same session, type in worldeventid in the box, change the drop down to Fields and hit the magnifying glass. The screen will change and the record will load. Now, type in wor. A search box will pop up with the search pre-executed, allowing you to pick from what you want. Think of this applied to a system for contacts, real estate, food management - the possibilities are endless.

Cool Feature - Recursive Screens and Searches

A pretty big part of the user experience is using screens. Some users only spend their entire work life in a company using only 3 or 4 screens. You want to reuse those screens whenever you can. Take a report for example. If you fire Ignition up and click on "Field Report" in the favourites bar. You'll get a pretty busy report, but if you double click on a row, you'll get a popup screen. This is the same screen that is used when you maintain modules. If you close that and expand one of the first three rows and double click that - you'll get a different screen.

The other cool thing are searches. When you are on a screen and do a search for a record, you get some search options. Example, if you click on "Search Definition Maintenance" on the Favourites Bar, and then click on the Search button, and then click on "Search All Search Definitions", you get a list of results back. Double click on any one and the record will load. Now go down to "Search Page Field". Click on the magnifying glass (or click into the field and press F3). A search box will pop up. These search options are the same options you get when you are on the field maintenance screen. Close that search screen.

Another cool thing is when you are in screen, you can sometimes edit another screen whilst searching for data. Take the search example. After closing the search dialog from above, go click on the magnifying glass for the "Search Script" field. Straight away the search will execute and you already have results. Now single click a row and then click on the Edit button. A new screen will popup allowing you to edit the record. How cool is that? All within the same instance. Not all search options have this ability and you need to manually set this up via Search Definition Maintenance. The reason for this is that you shouldn't actually have the ability to create or edit records on the fly, depending on the screen in question and the business processes surrounding it.

Cool Feature - WPF Functionality

Ignition was designed as a WinForms application. Along the way XAML came out and it's only recently that I've tried to support XAML/WPF in Ignition. The result - WPF screens are supported, but to use them, put your business logic in the webservice. It's the only way I could get around WCF calls from an XBAP (the ideal goal of using WPF). Once you start getting into XBAP [applications] it's a proper pain in the butt to communicate out of it. That alone really is an article in itself! I've only got one example and it's a pretty poor one.

To test the WPF screen, make sure you've got Fusion running (without basic bindings) and then go to the Misc module, then Other and then start the WPF Test screen. Click on New, go into any field, then click on Save. You should see some text in a box. That text was retrieved from a hosted webservice. It's actually reasonably complicated to set on of these screens up (because it's using webservices, you have to create bindings and also configure security). It's essentially putting a XAML screen in a WinForms application and then you are creating additional bindings for that screen. Not sure if this is the best way mind you. I have also tested WPF screens in a prototype XBAP, and it did work. Very cool seeing the same screen in the web as you do in WinForms world and also using the same business logic.

Quick Word on Caching

The Ignition WinForms client uses caching pretty heavily. Once you do a certain thing, the metadata is read from the DB and then cached for faster execution next time. Not everything uses it, but table and field metadata do and QuickSearch items do as well. If you change metadata and find the related functionality hasn't updated, close and restart Ignition. Clicking on the Home button also clears out most metadata, but not all. In a production sense this speeds up Ignition even faster.

Points of Interest

Points of interest, what I learnt? I learnt five years is a long time. I learnt some people go their entire lives without experiencing life. I learnt some people end up in careers they don't want to really be in and are only in it because circumstances makes its that way. I learnt many things. I learnt that although developing Ignition has come with its ups and downs, I don't regret the time I spent doing it. If there are budding developers out there thinking of doing something similar - go for it. You may come to a different conclusion than I have. You may create an awesome product and want to sell it. If you have an idea, no matter how big, do your best. Take it in small chunks.

By the same token, five years is also a short time. Life is short - do what you want to do.

In terms of programming, I've learnt how to implement WCF, how to use WPF in a basic sense, how to host WinForms controls and WPF controls in the same application. I've learnt how to dynamically create webservices. I've learnt how to use multicasting. I've learnt how to create a framework.

I've also learnt how to do things the wrong way. The Ignition client application is essentially a fat client. That's because it initially was a server/client system. Then I put in the webservices. I should have done the webservices first and the client should just be hosting the forms.

Some bits of the documentation may be called Draco or Apollo. The system has been through a couple namespace changes.

What is not done

As complete as Ignition is, some things are not complete. Major things being:
  • Schema creation - tables and fields - stuff like that.
  • Importing/Exporting of functionality.
  • Reporting - needs more work to make it complete.
  • Reporting - XtraReports need parent report capability.
  • Notifications - ability to send messages on updates to the database.
  • Documentation - not all done.
  • Custom URL entry on the logon screen of client - for work at home situations.
  • Messaging in client - framework is done, UI is not done.
  • Attachments should be stored in database.
  • Record concurrency - needs to be implemented.
  • Proper data encryption - right now, business logic handles encryption - should be in DAL.

I'm not saying I won't get around to these things, but it may be a while before that get implemented. I'm currently busy with other things at the moment, and I'm putting this up on CodeProject so other people can see how a framework could be written.

More info on License

The licenses still confuses me a bit (would never be a lawyer), but essentially I don't want people downloading the framework, rebranding it and then on-selling it as their own framework, closed source.

What I am fine with is people modifying the framework, keeping the framework open source, but - and this is a big but - you can create your own modules and then on sell your modules for profit. I guess it's a fine line between making money off the framework compared to making money off your modules that you would create.

I read as much as I could into the different licenses before I fell asleep, but the GPL was the closest I could find to this.

So put simply:
- I don't mind people modifying the framework
- I do mind people changing the framework and then selling the framework
- I don't mind people creating their own modules
- I also don't mind people selling those modules
- You can put Ignition in commercial scenarios

I hope that clears it up, I think that's quite reasonable - what I'm just trying to do is keep the open source vision clear and steady - any changes to the framework I believe should be free and available to everyone. What you do with the modules that plug into it, you can really do what you like.

If you have some really cool code you want to stay closed source, but need that code to distribute to commercial applications, you're free to do that. But if that code is required for the framework to operate, it should really be open source.

End Words

To finish, this project has been my enjoyment, my pain and my learning experience in the .NET world. I don't regret taking this project on, and feel like the community can benefit from some of the stuff in here. It's mostly written in VB.NET, so it should be easy to pick up and step into the code to see how she works.

My involvement with this project may become less and less over time. To start off with, I'm hosting the code on CodePlex as a 7zip download. If there is enough interest, I'll change it to be a subversion style. So the community can get into it and start changing it. I hope anyone who downloads it and gives it shot may learn something new.

It all depends on demand basically. If enough people start using this, the to-do list above is really required to be done to get Ignition into a situation where it can be put into production use. So if you download it, give it a twirl, like it - give us a yell - in my spare time I'll finish this baby off. I reckon there is about 3-4 months part-time work (like weekend) left to complete this.

And if you read through the article (it's a big ask - it's pretty big!) and look through the code and just want a bit of an explanation, or think it may make a good article, let me know and I'll try and write something up. If my style of writing is not too on the wacky side!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here