Archive for VB.NET

Highlighting DataGrid Rows

Posted in The Project with tags , , , , , on June 22, 2008 by moffdub

I have a story about one instance in which The Project‘s management’s refusal to upgrade from an old version of .NET cost them valuable developer time on a feature that is trivialized in subsequent versions.

When showing all of the equipment of a certain type, there were certain statuses, like “In use” or “Discarded”, that were not to contribute to the equipment count of a room. And in order to keep the counts consistent with the full listing, it would make sense not to show such equipment in the listings. But then you had no easy way to change something from “In use” back to “Spare”.

The solution was to show them anyway in the listing, but highlight their rows to set them apart from the rest of the equipment. Since the natural Windows Forms control for this task is a DataGrid, I set out on figuring out how to highlight specific rows of a DataGrid in .NET 1.1.

The version number is key. Being restricted to .NET 1.1, I was unable to use a DataGridView, which is a customizable DataGrid. This cut me off from handling the CellFormatting event of DataGridView and implementing a rather painless solution to the problem.

After a long search, I found an article that describes the solution. Since the article is fairly detailed and contains lots of other examples, I will summarize how to color a specific row of a .NET 1.1 DataGrid (refer to the article itself for code examples):

  • Define a new event with a new data type that will keep track of a cell’s row, column, background color, and font color. Make sure the new data type is passed by reference.
  • Handle this event in the form that contains your DataGrid. Do not use the handles keyword. This way, you can re-use the handler and you aren’t tied to a specific number of columns.

    In this handler, you provide the logic that determines which rows are colored. For us, we assume we have this info in a Hashtable somewhere. So we compare if the event’s row shows up in our Hashtable and is not selected. If so, set the background and font color properties of the event.

  • Inherit from DataGridTextBoxColumn and override the seven-argument Paint method. This method will get called every time a cell is drawn on the screen.
  • Now you fire the event you earlier defined, providing it with the current row and column, which you can get from inside the overridden Paint() and from Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me).
  • Finally, go back to your form and replace instances of DataGridTextBoxColumn with your derived column. Use the AddHandler statement to add the event handler to handle the derived column’s newly-defined event.

    The event is handled in the form and its argument gets altered. After the RaiseEvent statement, you can inspect the argument to see if it was altered and set the appropriate brush color to pass to MyBase.Paint().

All of that to highlight a lousy row.

Problems

Well, fine, it might be an annoying exercise in polymorphism and event handling for me, but it did get the job done.

But there was a problem. The columns of this DataGrid had to be sortable. As it stood after the above steps, if you clicked on a column, the grid would sort based on that column, but there wouldn’t be a re-painting of the rows, so the wrong rows would be highlighted after a sort.

Solution:

  • Keep track of a need-to-repaint flag.
  • Handle the DataGrid’s Paint event. Check the flag and re-paint if the flag is true. By “re-paint”, I mean iterate through each row, examine the correct column(s) value(s) that determine if a row needs to be highlighted, and add that row number to the Hashtable. Be sure to set the flag back to false or you’ll be painting forever.
  • Handle the DataGrid’s OnClick event, not its OnMouseDown event. Perform a HitTest and see if the click happened on a column heading. If yes, set need-to-repaint to true.

Why not handle OnMouseDown? I was never fully sure if I correctly understood this MouseClick article:

Depressing a mouse button when the cursor is over a control typically raises the following series of events from the control:

1. MouseDown event.
2. Click event.
3. MouseClick event.
4. MouseUp event.

and this MouseDown article:

Mouse events occur in the following order:

1. MouseEnter
2. MouseMove
3. MouseHover / MouseDown / MouseWheel
4. MouseUp
5. MouseLeave

but I think it is because MouseDown occurs before MouseClick, so in MouseDown, let the re-sort happen with no re-paint. Then, in MouseClick, do a re-paint; this avoids re-painting too early.

OK, that is solved, but now whenever I have a substantial amount of data in a grid, there is a “flickering” effect whenever the grid is loaded or a column is sorted on.

Solution: in DataGrid’s Paint handler, hide the grid before doing the re-paint and then re-show it once finished.

OK, that is solved, but now, whenever a very substantial amount of data is in a grid, sorting by a column introduces a considerable multi-second delay before the grid is visible again.

Solution: paginate the data. Problem: we are in Windows Forms, not ASP.NET.

So we’ve followed this to its logical conclusion: now we’ll have to implement a custom paging solution in order to side-step this performance problem. I never actually got around to implementing this feature, and possible solutions are the topic of a separate post.

So there you have it. Refusal to upgrade to a newer version of .NET, even though the platform itself is rather high-level, cost us substantial development and test time. Don’t underestimate the value of upgrades.

Domain-Driven Reports, Part 4

Posted in The Project with tags , , , , , on June 6, 2008 by moffdub

Approach

This is the third, last, and chosen approach to the problem of domain-driven reports.

Save all of its cons, the previous approach seems to me to be more “domain-driven” and preferable to an Anti-Corruption Layer approach. But what about those cons?

To me, the first big problem is needing to write the report logic ourselves. I’m talking about grouping, outer joins, counts, summation, etc. In other words, all of the stuff relational DBs are great at doing.

The second big problem is instantiating possibly a huge amount of domain objects in order to utilize them. This can, of course, be solved by using some kind of “external processing” technique, in a throwback to the days of the mainframe. But that is yet more code to write, and why write it when an RDBMS likely addresses this problem already?

So, in this approach, we should push the report logic back to an RDBMS. Specifically, instead of the Room Summary Report Repository being persist-only, flip the script and make it retrieve-only.

Retrievals from this “repository” will populate a table such that the rows of that table can be fed directly into a reporting engine to produce the formatted report. Optionally, and by default, the retrieval operation will also return the domain object representation of the report.

Example

We will re-use the RoomSummaryReport and RoomEquipmentSummary objects from the previous approach. The only re-write is the ReportService.

The ReportService is outfitted with five, count them, five possible output formats: PDF, Snapshot, String, domain object, and DataSet. For the DataSet, we need to essentially do a SELECT * FROM X query because we are essentially using a .NET DataGrid as a viewer of data that is already ready to view (just like Access, without all the formatting capability).

Dogmatically, we should create a new class, perhaps DataSetReportViewer or something, that is responsible for this, because this really isn’t a Repository function. Since the code that follows is already a mouthful, I implement this in a retrieveDataSet() method in the RoomSummaryReportRepository.

First, an overview of what the class and method signatures look like now:

Public Class ReportService

	Public Sub New()

	End Sub

	Private Function runReport(ByRef user As User, _
		ByRef rooms As Room()) As RoomSummaryReport
		
		Dim reportRepo As RoomSummaryReportRepository = _
			RoomSummaryReportRepository.getReference()
			
		' by default, retrieve will populate the formatted table
		' AND return a RoomSummaryReport object; this is what the
		' boolean argument controls. runReport can expose this
		' to callers if needed in the future
		Return reportRepo.retrieve(user, rooms, False)
		
	End Function	

		
	Public Function generate(ByRef user As User, _
		ByRef rooms As Room()) As RoomSummaryReport
		...			
	End Function
	
	Public Function generateText(ByRef user As User, _
		ByRef rooms As Room()) As String
		...		
	End Function
		
	Public Function generatePDF(ByRef user As User, _
		ByRef rooms As Room()) As String
		...
	End Function
	
	Public Function generateDataSet(ByRef user As User, _
		ByRef rooms As Room()) As DataSet
		...		
	End Function
	
	Public Function generateSnapshot(ByRef user As User, _
		ByRef rooms As Room()) As String
		...
	End Function

End Class

First, a function to get a report object:

	Public Function generate(ByRef user As User, _
		ByRef rooms As Room()) As RoomSummaryReport
		
		Return runReport(user, rooms)
		
	End Function

Next, a function to generate a text report:


	Public Function generateText(ByRef user As User, _
		ByRef rooms As Room()) As String
		
		Dim retStr As String
		
		retStr = "Room Summary Report" & vbCrLf & vbCrLf
		
		Dim report As RoomSummaryReport = _
			runReport(user, rooms)
			
		Dim summaries() As RoomEquipmentSummary = _
			report.getSummaries()
			
		Dim roomEquipSummary As RoomEquipmentSummary
		
		For Each roomEquipSummary In summaries
		
			retStr = retStr & roomEquipSummary.getUser().getFullName() & _
				" has entered the following equipment " & _
				" in room " & roomEquipSummary.getRoom().getName() & _
				vbCrLf & vbCrLf
		
			Dim equipTypes() As String = _
				roomEquipSummary.getTypes()
				
			Dim equipCounts() As String = _
				roomEquipSummary.getCounts()
				
			Dim i As Integer
			
			For i = 0 to equipTypes.Length
				retStr = retStr & equipCounts(i) & " " & _ 
					equipTypes(i) & "s" & vbCrLf
			Next i
		
		Next roomEquipSummary
		
		Return retStr
		
	End Function

Let’s suppose the PDF engine can, somehow, be told how to generate a report from our report object. This function demonstrates how we aren’t tied down to row-based reporting engines with this approach.


	Public Function generatePDF(ByRef user As User, _
		ByRef rooms As Room()) As String
					
		Dim report As RoomSummaryReport = _
			runReport(user, rooms)
			
		Dim PDFEngineService As PDFReportGeneratorService = _
			PDFReportGeneratorService.getReference() 
			
		Return PDFEngineService.Create(report)
		
	End Function

DataSet… for display within the application…


	Public Function generateDataSet(ByRef user As User, _
		ByRef rooms As Room()) As DataSet
		
		' this populates the table, called X, with the
		' formatted ready-to-display report
		Dim report As RoomSummaryReport = _
			runReport(user, rooms)
			
		Dim reportRepo As RoomSummaryReportRepository = _
			RoomSummaryReportRepository.getReference()
			
		' retrieveDataSet here kind of acts like Access
		' does in that it just displays the contents of
		' a table that contains the formatted report
		' ...unlike Access, that is all it does
		
		' this could easily be replaced by a "SELECT * FROM X..."
		Return reportRepo.retrieveDataSet()
		
	End Function

Finally, a Snapshot format:


	Public Function generateSnapshot(ByRef user As User, _
		ByRef rooms As Room()) As String
		
		Dim SNPFileName As String = "RoomsReport" & user.getFullName() & ".snp"
		Dim rptName As String = "RoomsReport"
		
		Dim fileName As String = ' from some config file
		
		Dim access As New Access.Application
		access.Visible = False
		access.OpenCurrentDatabase(fileName)
		
		' this populates the table, called X, with the
		' formatted ready-to-display report
		Dim report As RoomSummaryReport = _
			runReport(user, rooms)
				
		Dim outPath As String = System.Environment.CurrentDirectory & _ 
			"\" & SNPFileName
		
		access.DoCmd.OpenReport(rptName, acViewPreview)
		access.DoCmd.OutputTo(acOutputReport, rptName, _
			acFormatSNP, outPath)
			
		access.CloseCurrentDatabase()
		
		Return outPath
		
	End Function

Pros

By producing both a row-based representation and a domain object representation,

  • We inherit the maintainability and clarity of the pure DDD approach.
  • The Report Service remains domain-driven because it returns domain objects in addition to taking them in.
  • The performance problem is avoided because the actual generation of the report is done in the DBMS, and in the case of The Project, server-side. Only the data for the report itself is sent back to the client. Loading potentially huge amounts of data to generate the report is no longer an issue.
  • We have assumed, throughout this series, that the reporting engine is row-based. If, down the road, our row-based engine is complemented or even replaced by one that operates in a different paradigm, we’re covered.

    In the example, I implement methods for textual, Snapshot, and PDF reports to emphasize the flexibility provided by having objects returned. We didn’t have this benefit with the Anti-Corruption Layer approach.

Cons

  • Still, we are mis-using the Repository idea. Now, we are in a retrieve-only situation.
  • Still, we could possibly face a Vietnam of OR/M mapping since we have a Report Repository moving between tables and domain objects.
  • The retrieval operation has a side effect. It is at least declared and made publicly known to consumers. This makes it possibly testable. Also, side effects are turned off by default, forcing consumers to explicitly state they want a side-effect-only function.
  • If you implement this with stored procedures, as I did, then you could argue that some of the report logic is still split across a process boundary, but I think this con is tenuous at best. The same argument can be made for all of my hand-written OR/M code, which I also wrote as s-procs.

I believe we have ended up with an acceptable list of cons here for formatted domain-driven reports.

Whew, that was a doozy. Problems always seem to occur at layer, technology, and paradigm boundaries. If only we could stay in fluffy flexible object land for everything.

Domain-Driven Reports, Part 3

Posted in The Project with tags , , , , , on June 3, 2008 by moffdub

Approach

This is the second approach I considered as a solution to the problem of domain-driven reports. Instead of trying to fit the status quo way of reporting in The Company into DDD, let’s instead try to fit the idea of a report.

Let’s continue with the idea of a Report Service. The Report Service should take in various domain objects as input (in our previous example, a User object and a collection of Rooms) and return domain objects as output.

Now, what is the nature of this output? It may seem counterintuitive to attempt to model a Report object. On the surface, it seems manufactured. However, Eric Evans, the man himself, employed a similar approach in his canonical shipping example. In a presentation (30 minutes in), Evans reforms a Routing Service that wrote into a database into a Service that created a new Itinerary object.


(recall from part 1)

It might be the case that this works in the shipping domain because itineraries have meaning to the domain experts. There are examples of other itineraries in other domains. The idea does not seem manufactured.

On the other hand, creating a Room Summary Report domain object might seem fabricated. It did, in the case of The Project, because domain experts didn’t toss around the term “room summary report” much. It was a new idea, but I think that had The Company treated The Project like it was a full-fledged software project, this term could have and would have entered the ubiquitous language.

That said, the approach is to follow Evans: create a Report Service that takes in a User object and a collection of Rooms and returns a Room Summary Report.

Model a Room Summary Report object, containing a collection of Room Equipment Summaries. Each Room Equipment Summary is composed of

  • a User object for which the Room Summary applies
  • a Room object for which the Room Summary applies
  • a collection of string-integer pairs, the string indicating the equipment type and the integer indicating a count of that equipment type for this Room and User

Finally, to take advantage of a system like Access, or any other row-based reporting engine, create a Room Summary Report Repository to persist the report in a way (de-normalized) that makes easy for that engine to format the report. This implies that the repository should not be used to retrieve Room Summary Reports. They should be generated by the Service.

Example

Foregoing the UML diagrams, I give you some sample implementations. First, the RoomSummaryReport domain object:

Public Class RoomSummaryReport
	Private summaries As IList
	
	Public Sub New()
		summaries = New ArrayList
	End Sub
	
	Public Sub addSummary(ByRef summary As RoomEquipmentSummary)
		summaries.Add(summary)
	End Sub
	
	Public Sub removeSummary(ByRef summary As RoomEquipmentSummary)
		summaries.Remove(summary)
	End Sub

	Public Function getSummaries() As RoomEquipmentSummary()
		Dim retArr(summaries.Count - 1) As RoomEquipmentSummary

		Dim i As Integer = 0

		For Each obj In summaries
			retArr(i) = obj
			i = i + 1
		Next obj

		Return retArr
	End Function
	
End Class

Now, the RoomEquipmentSummary domain object.

Public Class RoomEquipmentSummary
	Private room As Room
	Private user As User
	
	Private Structure EquipTypeCount
		Dim equipType As String
		Dim equipCount As Integer
	End Structure
	
	Private counts As IList
	
	Public Sub New()
		counts = New ArrayList
	End Sub
	
	Public Sub addEquipCount(ByVal equipType As String, _
		ByVal equipCount As Integer)
		
		Dim toAdd As New EquipTypeCount
		
		toAdd.equipType = equipType
		toAdd.equipcount = equipCount
		
		counts.Add(toAdd)
		
	End Sub
	
	' other requisite getters and setters ...
End Class

Finally, the ReportService, complete with simple report logic:

Public Class ReportService

	Public Sub New()

	End Sub
	
	Public Function generateReport(ByRef user As User, _
		ByRef rooms As Room()) As RoomSummaryReport
		
		Dim roomRepo As RoomRepository = _
			RoomRepository.getReference()
			
		Dim roomsOfInterest() As Room
		
		roomsOfInterest = roomRepo.retrieve(user)
		
		' we now have all rooms for which
		' user has entered equipment
		
		Dim equipTypeEnum As DynamicEnum = _
			EquipmentTypeEnum.getReference()
			
		Dim equipTypes() As String = _
			equipTypeEnum.convertToStrings()
		
		Dim returnedReport As New RoomSummaryReport
		
		Dim r As Room
		
		For Each r In roomsOfInterest
			Dim roomEquipSummary As New RoomEquipmentSummary
			
			roomEquipSummary.setUser(user)
			roomEquipSummary.setRoom(r)
			
			Dim equipType As String
			Dim equipCount As Integer
			
			For Each equipType In equipTypes
				equipCount = r.getEquipCount(equipType, user)
				roomEquipSummary.addEquipCount(equipType, equipCount)
			Next equipType
			
			returnedReport.addSummary(roomEquipSummary)	
		Next r
		
		Return returnedReport
		
	End Function

End Class

Some commentary here: First, why go with ILists in some classes and native arrays in others? No reason, could go either way.

Second, depending on the implementation of getEquipCount in the Room object, you might have several database calls jumping back and forth to get a count of a specific equipment type for each equipment type. Now, might not be a problem depending on your deployment environment (see Cons section below). Also, the object itself might be instantiated with the counts upon reconstitution in the Room Repository. In that case, there wouldn’t be a jump to the DB.

I will also forgo the Room Summary Report Repository, but I will give a sample of what the underlying table looks like, a table which can be fed into Access or the like to create the formatted report.

UserName RoomName EquipmentType Count
MoffDub Den Hats 24
MoffDub Den Jerseys 16
MoffDub Den PCs 10
MoffDub Den Textbooks 21
MoffDub Den DVDs 3
MoffDub Bedroom Hats 12
MoffDub Bedroom Jerseys 5
MoffDub Bedroom PCs 0
MoffDub Bedroom Textbooks 11
MoffDub Bedroom DVDs 5

Pros

  • From a dogmatic POV, this is a true DDD Service.

    From a practical POV:

  • This is likely to be easier to maintain. If we need to produce multiple interfaces for this report, like a ToolTip capability, we have an in-memory representation of the report as well we can use.
  • Having an in-memory representation makes it easier to re-use it to create tabular or textual representations of the report.
  • This is likely to produce clearer code.

Cons

This has serious practical problems:

  • We have introduced more code to be written.
  • The report logic now has to be hand-written. This is not a huge problem for a simple report such as this, but if it gets more complex, you’re in trouble.
  • More generally, this places a stress on the runtime memory if the amount of domain objects to summarize is potentially vast. This has performance implications, especially if, as in the case with The Project, the database server and the Report Service live on tiers separated by a network.
  • The potential for complexity can get nasty when it comes time to flatten the Report object via the repository, though this isn’t as serious of a OR/M problem as a true DDD Repository.
  • We’ve kind of abused the idea of a Repository because we can only persist with it. Perhaps it would be better named as a Service, but then you have a Service touching the DB. So we’ll settle with a semantically modified Repository.

The performance problems, combined with time constraints, convinced me to ditch this solution. Specifically, I couldn’t stomach coding all of the report logic myself. I have an external tool that excels in such logic. I can write the report in a somewhat declarative way (Access SQL), and be done with it. It didn’t seem like a wise engineering decision for my situation.

Don’t get this twisted. The nice DDD OOA&D way outlined here may work if the time constraints hadn’t existed and I had more control of the deployment environment. What remains, then, is to weigh the benefits of coding the report logic versus offloading the logic to a reporting engine.

OK, the next and final part of the domain-driven reports series will build on this post and present the solution I went with.

Follow

Get every new post delivered to your Inbox.