I recently read this post by Rocky Lhotka about the importance of where to place business rules when designing software. My reply, along with an answer to my own question, is reposted here.
I must say I agree with you on this topic; I don't know anyone who doesn't. I have developed a few applications in VB 6 (yes, VB version 6) and C# using this approach.
It IS very, very difficult to implement in any but the most simple of applications, however. Unless you are very careful, it is very easy to have all your layers working properly and then still find business rules being placed in screens, forms, and services. In the majority of business applications that I've put together, the same business object is used in multiple screens, forms, and services. The problem is that, usually, each one requires that different - conflicting - sets of business rules be placed on the same object. There are many design choices when faced with this situation.
1. You can wire each screen, form, or service with the rules. Obviously, some of the rules will be duplicated. This is exactly what we are trying to avoid. It is, however, the easiest to implement; especially since many UI actions occur based on the results of some of the business rules. It's a trap! Don't fall for it. 2. You can clutter the object with "condition" settings and apply rules based on the conditions. I've seen this done many times and I hate it. Talk about a maintenance nightmare... 3. If the screen, rule, or service requires a special set of business rules, you can create a separate business object for each; inheriting from the business object that provides the most complete set of rules for the designed purpose. This sounds good. Business Objects contain all the business rules. Screens, forms, and services don't have duplicate rules to maintain. Maintenance can still be a pain in some cases, though. When a new rule must be created, care must be taken to position it in the right business object so that it is inherited only where required. It is very easy to find duplicate rules in this setup after a few months of maintenance kicks in.
Of these options, I like number three the best. But I find that most people first choose option 2, then find themselves wishing for something different.
If anyone knows of other options or approaches, I'd love to hear about them.
Now all this said, I have to laugh at this statement from your post, Rocky: "The resulting applications are very fragile and are impossible to upgrade to the next technology (like .NET). Today, as we talk, many thousands of lines of code are being written in Windows Forms and Web Forms in exactly the same way. Those poor people will have a hell of a time upgrading to WPF, because none of their code is reusable."
This strikes me as "funny" because every time I've upgraded an application to the next technology, it required a complete rewrite from the ground up! The rewrites aren't necessary because business rules are placed in forms, screens, and services - but because the code wasn't transferable to the new technology! Even upgrading from .NET 1.1 to .NET 2, or .NET 2 to .NET 3, how much of the code is going to transfer cleanly? That's just code. Usually an upgraded database is involved, too. SQL Server 2000 to 2003 or 2005, for example. Coding technology, databases, and even hardware are changing so rapidly that keeping up with it all is practically impossible.
Really, does ANY of this really matter?
My Answer: Yes! It does matter. Some day, hopefully sooner than later, there will be a "universal" understanding of how to design and write code for these situations. I thought XML, or some cousin, had a chance of helping in this arena. Perhaps it still does. For now, though, it seems to have hit a wall.
All the discussing and swapping of ideas leads us closer to this magical day.
I've seen many posts and articles on how to create a modal dialog for a web application. They all describe different ways to acheive this goal. Some articles even offer controls that you can drag onto the page. I tried these with varying levels of success. That is to say, the more complex the page, the less reliable these controls. So...
I'm going to describe, step by step, how I accomplish modal dialogs for my web applications written using .NET v1.1, C#, and Inernet Explorer v6.0. I see no reason why this wouldn't work for VB as well. If anyone can add to this or explain why this wouldn't work for other browsers, I'd love to hear from you.
Step 1: Create the dialog page. I should be setup just like any other web page with the following exceptions:
Normally, we don't want a dialog to be cached because it is customized for certain situations. In the directive section of the page (HTML view, top of the page), enter a directive to disable the caching for the page.
- <%@ OutputCache Location="None" VaryByParam="None" %>
In the <HEAD> tag, set the target for the page to itself. This prevents a blank page from displaying when the page is closed.
Step 2: We need a way to close the dialog reliably. Response.Redirect() won't work for obvious reasons. Inside the event that will close the dialog, normally a "cancel" button click, register script that will close the window. If you have multiple ways to exit the dialog, you might want to create a seperate method and call it from each event.
string strScript = "<script language='javascript'>self.close(); </script>"; if (! this.IsClientScriptBlockRegistered("CloseScript")) this.RegisterStartupScript("CloseScript",strScript);
Step 3: Create the page that opens the dialog. In the button or link that requires the modal dialog, setup the script that will open it for you. I use the syntax "script defer" to allow the page to finish painting before opening the dialog. "Defer" is not necessary and might not work in your situation, but it doesn't hurt to include it. Make sure to include code that submits the form to allow processing of the dialog results. By default, the form name is Form1, so the script is document.Form1.submit(). If you are creating a dialog the only displays information, then the document.Form1.submit(); script is not necessary.
string strScript = "<script defer>"+ "var strReturn = window.showModalDialog('MyModalDialog.aspx?Parameter1=" + parameterVariable + "','','dialogWidth:520px;dialogHeight=600px');"+ "document.Form1.submit(); </script>";
if (! this.IsClientScriptBlockRegistered("OpenMyDialog")) this.RegisterStartupScript("OpenMyDialog",strScript);
Of course, if the dialog doesn't required parameters then you don't need to set them up. Set the dialogWidth and dialogHeight properties appropriately.
Step 4: All that's left is to setup a way to determine what the user did inside the dialog. Dialogs have many different uses, so this is where you can get creative. Some dialogs may enter information into a database, some may be as simple as answering a yes or no question. Still others may only be displaying information. If I have a dialog that gathers data for entry into a database, I usually let the dialog handle the actual entry instead of passing the data back to the calling form. This allows me to display any rule breaks or errors to the user without loosing track of what they were entering. That way, they don't have to start all over with a blank form. If the application is more complex, then you might need the main form to process the information before saving it to the database.
In almost all cases, the main form needs to know what the dialog has done. So the approach I use works for nearly every situation. I create a simple serializable object, DataTable, or DataSet (depending on the complexity of the dialog) to put into Session. The main form normally seeds the variable with information. The dialog can use the variable however is necessary. In nearly every case, in the event that closes the dialog, the Session variable is modified and saved back into Session. The calling form then uses it's Page_Load() event to check the variable and take appropriate action.
The dialog "accept" event code:
MyDialogObject DialogReturn = new MyDialogObject(); ... save to database, fill DialogReturn properties, etc. Session[MYDIALOG] = DialogReturn;
CloseWindow(); // Step 2 above
The main (calling) form code:
private void Page_Load(object sender, System.EventArgs e) { if (! IsPostBack) { ... whatever ... }
// If a dialog just closed, do whatever is necessary; if (Session[MYDIALOG] != null) { ProcessDialogInfo(); } }
All Done! Creating modal dialogs with C# and .NET is not as complex as it might seem at first glance. Well, ok - aquiring all this knowledge and information and putting it to use was harder than it should've been. But now it's all in one place. Comments and suggestions are welcome.
Rockford Lhotka has posted a very nice article on some tough issues with SOA. Please read the post and then check my thoughts below:
Very well written. I have a question, though. If we need to perform the follwing actions - trigger invoicing, contact the customer, print pick lists, and update the customer's sales history - why would we call a service that *might* do these actions.
Wait, I think I can answer this one myself. You are talking about systems that are already in place, have been for quite some time, and have evolved to do these things. Then people start using the service and either need something else done or don't need all the "services" the object provides.
This is a very common problem and is certainly not limited to SOA. All developers have had to fix "established" code that has these problems. What you end up with is code that resembles a ball of twine and you're darned lucky if it doesn't have more than a few knots inside.
How do you solve the problem? Some would say to force the service objects to perform one and only one service. This is all well and good. But then you hit the overhead problem you so very well described.
What I have determined is this. Each application has a purpose and needs "services" performed. Other applications may need similar services. But the second you decide to combine the services provided to both applications into one object, you open the door to unexpected behavior. Heck, sometimes (usually) even the same application needs a service to behave differently for various situations.
Ever need to open a modal dialog in a web application? Unless you've only written the simplest of applications, then the answer is a resounding "YES!" There is certainly more than one way to accomplish this. But this particular answer suited my needs perfectly.
What I needed: Display a list of items for the user to choose in a modal window. If the item they require is not available, then allow them to add the item, with extra details. In this case the item represents a company, and the extra details are the company address, phone numbers, fax, notes, etc. Sounds easy enough.
There were two problems, though.
Modal Form Opens a Blank Window Closing the window causes another - blank - window to open in it's place. Weird but easily solvable. Simply place this code in the <HEAD> section of the modal form: <base target="_self"> Now the the blank window is no longer opened.
The Modal Window is Cached! The content of the window is cached on the server. So when it is opened for the second time, the Page_Load event is not fired. The problem for me was that the contents of the combo box were not refilled, so any new companies that were added manually weren't reflected in the list! I saw a multitude of answers while searching, but this one seemed to make more sense to me than the others. In the declarations section of the form (very top of the HTML code) enter the following line: <%@ OutputCache Location="None" VaryByParam="None" %>
I should mention that I did find numerous other answers to the caching problem that I did not try. If the above answer doesn't fit your situation, you might try some of these:
add this to the modal window's page_load: Response.Expires = 0; Response.Cache.SetNoStore(); Response.AppendHeader("Pragma", "no-cache");
Or this: Response.Cache.SetNoStore(); Response.Cache.SetExpires(DateTime.Now.AddMilliseconds(-500)); Response.Cache.SetCacheability(HttpCacheability.NoCache); Response.AppendHeader("Pragma", "no-cache");
Or any combination of the above. I did not require any code in the page_load method.
In my last post, I showed you how to navigate a self-joined table to
create a list of primary keys. Now I will show you how to put the code
into a UDF to return the list.
It is quite easy to create a User Defined Function that will return
table variable. Using the information from the last post, here is the
setup. Note that the table is defined right in the RETURN definition:
CREATE FUNCTION dbo.udf_BookingCommodityList
(
@BookingId int
)
RETURNS @CommodityIdList TABLE (
BookingCommodityId int )
AS
BEGIN
<... Rest of function >
The cool thing about this is that, with the table defined in the RETURN
definition, we can use it throughout the UDF as though we created it
with a normal DECLARE statement.
So now that a table of primary keys is being returned, how can it be
used. Well it turns out that the UDF acts almost just like any other
table in SQL Server. For example, this statement works just fine: SELECT * FROM dbo.udf_BookingCommodityList(12345). This will return all the commodity ids for booking number 12345.
Using the UDF in a statement is pretty straight forward:
-- @BookingId is passed in as a parameter
SELECT commodity.Description
FROM dbo.udf_BookingCommodityList(@BookingId) bc
INNER JOIN Commodity ON (Commodity.BookingCommodityId = bc.BookingCommodityId) This returns the commodity descriptions used on the booking. This is great as long as the BookingId is known in advance (i.e. passed in as a parameter or derived in some other fashion; cursors, etc.). This statement does not work to my dismay. I haven't figured out why just yet. SELECT commodity.Description
FROM Booking
INNER JOIN dbo.udf_BookingCommodityList(Booking.BookingId) bc
INNER JOIN Commodity ON (Commodity.BookingCommodityId = bc.BookingCommodityId)
WHERE Booking.BookingId = 12345
Situation:I have a SQL Table that is self linking. Records act as parent and child. In this case, it is for a packaging system where it is possible to have multiple packages containing a commodity (product). For example, a pallet containing 50 boxes, each box contains 24 bags of potato chips. The application allows for an unlimited number of package to package relationships. Here's a sample diagram from SQL Server.  Problem:The customer requires a report where only the commodity is displayed. The commodity may be burried very deep in the chain of packages. And because the commodity is only aware of the immediate package above it (using the example above, the boxes), getting to the commodity can be difficult. To help illustrate, here's a perfectly valid example from the application.  This is a very complex pallet to say the least. Probably one that would never happen, but it is still "valid" as far as the application is concerned. The report requirements state that, given the CargoLineId (represented by the green ball, pallet), list the commodities. The report is not interested in displaying any details about the packaging. Solution:Using the self-join of table BookingPackage we can determine the immediate children of each record. The child package will have a ParentBookingPackageId that matches the parent BookingPackageId. Each package may or may not contain a commodity. What we must do is walk our way down the tree of packages to get to the commodities. The solution presented here could be used differently depending on the solution required. Use the example as a guide and change it to suit your particular needs. Here is the code in T-SQL: /* Create a temporary table to hold all PackageIds. */ DECLARE @PackageIdList TABLE ( PackageId int ) /* Create a temporary table to hold the CommodityLineIds. This table eventually contains every Commodity attached to the booking and is used as the result set. */ DECLARE @CommodityIdList TABLE ( CommodityLineId int ) /* Insert the top level packages @BookingId is passed in as a parameter */ INSERT INTO @PackageIdList SELECT BookingPackageId FROM BookingPackage pkg INNER JOIN BookingCargoLine cargo on (cargo.BookingCargoLineId = pkg.BookingCargoLineId) WHERE cargo.BookingId = @BookingId /* Get a count of second level packages */ DECLARE @pkgCount intSET @pkgCount = ( SELECT COUNT(BookingPackage.BookingPackageId) FROM BookingPackage INNER JOIN @PackageIdList list on (list.PackageId = BookingPackage.ParentBookingPackageId) WHERE BookingPackage.BookingPackageId NOT IN ( SELECT PackageId FROM @PackageIdList) ) WHILE @pkgCount > 0 BEGIN /* Insert the package list into the temporary Package table If the PackageId is already collected into the temporary table, do not add it. With each pass, sub-packages are added to the list. */ INSERT INTO @PackageIdList SELECT BookingPackage.BookingPackageId FROM BookingPackage INNER JOIN @PackageIdList list on (list.PackageId = BookingPackage.ParentBookingPackageId) WHERE BookingPackage.BookingPackageId NOT IN ( SELECT PackageId FROM @PackageIdList) /* We just added packages to the list, check to see if there are more to add. Eventually, we will get to the lowest level of sub-packages and the count will be zero; ending the WHILE loop. */ SET @pkgCount = ( SELECT COUNT(BookingPackage.BookingPackageId) FROM BookingPackage INNER JOIN @PackageIdList list on (list.PackageId = BookingPackage.ParentBookingPackageId) WHERE BookingPackage.BookingPackageId NOT IN ( SELECT PackageId FROM @PackageIdList) ) END/* Now that we have a list packages, we can derive the Commodities that belong to each one. */ INSERT INTO @CommodityIdList SELECT BookingCommodityId FROM BookingCommodity WHERE BookingPackageId IN ( SELECT PackageId FROM @PackageIdList) /* Now we have a table of commodities that we can use in the rest of the procedure as a joining table to retrieve the commodity details for the report.
*/
*** END OF T-SQL The secret to this working is the WHILE loop and the table @PackageIdList being used as a filter to determine the depth of the package tree. We are inserting into the @PackageIdList table and, at the same time, using it as an inner join for parent packages. The where clause is used to make sure we don't pump duplicate values into the table. INSERT INTO @PackageIdList
SELECT BookingPackage.BookingPackageId
FROM BookingPackage
INNER JOIN @PackageIdList list on (list.PackageId = BookingPackage.ParentBookingPackageId)
WHERE BookingPackage.BookingPackageId NOT IN ( SELECT PackageId FROM @PackageIdList)
The final step is to fill a table variable, @CommodityIdList, with the list of commodity ids belonging to the main cargo line, or package. INSERT INTO @CommodityIdList SELECT BookingCommodityId
FROM BookingCommodity
WHERE BookingPackageId IN ( SELECT PackageId FROM @PackageIdList)
If we wanted, we could have put a lot more information into this table. I decided to use it as a joining table to get the details I need later on in the procedure. Conclusion:We end up with a result that mirrors recursion in T-SQL. We didn't have to use cursors or extra procedures to make it work. Now, if we were only using SQL Server 2005, we could achieve the same result with one statement using the WITH clause. I'll find a good example and post on that next.
I just read a very nice article on ASP.NET Best practices by Ali Khan. It covers some items that most .NET developers should know by heart and others that we do know but sometimes forget.
The only thing I can find to disagree with is tip number 15. It talks about using stored procedures instead of "ad-hoc" queries. The speed benefit of this approach has met skepticism lately by numerous developers. I definitely fall into this category. I prefer keeping the maintenance of queries outside of the database.
So now you have a nice report written to an HTML file. How do you open it for the user to view it using IE? You could try this:
System.Diagnostics.Process.Start("iexplore.exe","MyReport.htm");
But this hasn't always worked for me. Here is a sure fire way to put it all together.
Add a project reference to the COM library, Microsoft Internet Controls. Add these two using statements:
using SHDocVw; using System.Runtime.InteropServices;
Where you need to open the browser, add the following code:
explorer = new InternetExplorer(); if (explorer != null) { explorer.Visible = true; object x = null; explorer.Navigate(@"MyReport.htm", ref x, ref x, ref x, ref x); }
Thanks to a good friend of mine, Micheal Beall of Overdrive Technologies, for helping me with this.
***Originally posted on DotNetJunkies on February 16, 2005
Printing a web page that represents a report has been difficult for me in the past. I want to print a header and/or a footer that repeats on every printed page. The screen version certainly isn't paginated, so how can you print a good looking report? Style (or CSS) to the rescue.
Put the following Style tag, or something similar, in the header of the page.
<STYLE TYPE=”text/css” MEDIA=”screen, print”> <!-- TABLE { table-layout: fixed; border: 0; cellspacing: 1; cellpadding: 1; font-family: Arial; font-size: 8pt; } TH { font-family: Arial; color: black; background-color: lightgrey; text-decoration: underline; } THEAD { display: table-header-group; } TFOOT { display: table-footer-group; } --> </STYLE>
Since nearly every report uses an HTML Table, this works very well. The THEAD and TFOOT styles is what makes the table a report. If you don't want to setup a style tag, you can enter the style right into the table.
<table style="table-layout:fixed"> <colgroup> <col width="150"/> <col width="100"/> <col width="150"/> </colgroup> <thead style="display:table-header-group"> <tr> <td>Header column 1</td> <td>Header column 2</td> <td>Header column 3</td> </tr> </thead> <tbody> <tr> <td>Body column 1</td> <td>Body column 2</td> <td>Body column 3</td> </tr> </tbody> <tfoot style="display:table-footer-group"> <tr> <td>Footer column 1</td> <td>Footer column 2</td> <td>Footer column 3</td> </tr> </tfoot> </table> Sounds too easy to be true but it works very nicely. Of course, adding more style (bolding, underlining, background and foreground colors, etc.) makes this a very nice reporting option.
***Originally posted on DotNetJunkies on February 16, 2005
After examining numerous articles on printing with C#, I was about ready to toss in the towel and just copy & paste to Word (yuck). Then I found How To Print the Content of a RichTextBox Control By Using Visual C# .NET on Microsoft's support site.
I was sure that printing a RichTextbox should be pretty easy. All the articles made it very difficult, though. This article shows the “correct” way to print. The entire contents are printed; pictures, other graphics, colors, etc.
***Originally posted on DotNetJunkies on January 11, 2005
|
Copyright © 2010 Mark Bonafe. All rights reserved.
|
|