Wednesday, February 11, 2009

Microsoft Access 2003 and Printer Settings (DEVMODE)

The least exciting part of my day is dealing with legacy Access applications.  I get saddled with it because when our IS department got shrunk last November, that set of projects was "most closely aligned" with the set of projects I currently worked on (see the Guerilla BI post).  Well, I've spent the last week trying to force Access to do some (I think) very basic printer configuration, and have been shocked by how inconsistent, confusing and plain WRONG a lot of the official documentation and "helpful" suggestions are out there.
So in the interests of "setting the record straight", hoping I can spare someone else this pain, and providing a spot to base my upcoming conversation with Microsoft on, I will now lay out what worked for me and what didn't.  I'll restate this caveat later as well: this works for me on Windows Server 2003 SP1 with Microsoft Access 2003.  Your mileage may vary - just as mine did.
The Business Problem
An Access application needs to print some documents to a shared network printer (shared with other applications and users).  This shared printer is not the logged on user's default printer, and the default tray of the printer does not contain the paper I need for this print job - A special tray on non-default printer contains the right paper.
The Technical Problem
#1 Access is Printer Ignorant
First, Access has very few ways to interact with the printer programatically.  In order to do things like change the page orientation or tray, you have to resort to using the DEVMODE structure - a dusty and decrepit object from the Win32 API that (somehow) has still managed to hang on to life.
There are quite a few Microsoft Support KBs, blog entries, articles, and forum topics that suggest code for interacting with the DEVMODE, or other ways to accomplish the feats of changing paper orientation and tray.  Unfortunately for me, only the DEVMODE route could change "media type" (AKA "paper type", such as "transparency" or "bond") - as the KB articles that had suggestions for orientation and tray used functions that didn't support media type.  (It appears that media type didn't arrive on the scene until NT 4.0 - likely due to the advent of laser printers that supported different fuser intensities for different materials.)
#2 Access is Inconsistently Printer Ignorant
Despite not having any built-in functions to programatically check and set printer settings, Access supports saving printer settings to the Report object you're previewing through user interaction with the Page Setup dialog.  You preview the report, change the printer and/or printer settings, and when you save the report, Access saves the printer and accompanying settings (including media type!) with the report.  The next time the report is printed, it uses those settings. 
Again, (IMO) Access is showing it's non-Enterprise suitability here.  If you're building a single-user, single-purpose application, that functionality alone is fantastic.  It's an easy way to configure your report, save the settings, and never have to worry about it again.
However, in an "Enterprise" setting, MS Access can't cut it.  (And oh, how it hurts to hear "Enterprise" and "MS Access" in the same sentence.)  Aside from the problems all Office apps have with traditional server environments, this business case presents another "flaw".  (But I can't blame the Access devs for this - they don't (I hope) intend for Access to be used in large environments.)  In my case, to support the business process, I need to set up the same report for different locations/users/printers.  Through other wizardry (ahem - login) I know what printer I should use, and I happen to have a table in the database with the desired tray, paper type, and other printer settings I want to use.  So I basically have to "override" the saved report settings with stored settings.  However, there is no Access interface to manipulate those saved settings.  Access saves the printer settings as a byte-array cast of the DEVMODE structure.
#3 Microsoft is Very Confused About DEVMODE
I've programmed the Win32 API before with VBA/VB6 - I wasn't afraid!  Setting up DECLAREs for API functions and defining TYPEs to mirror API C-language struct definitions wasn't foreign to me.  How hard could it be?
To be honest - a f***ing nightmare if the documentation is wrong.  Get this - in several hours of searching (on and off as things looked like they'd work for me, then didn't) I came across SEVEN different definitions of DEVMODE from Microsoft.  And no, I'm not saying they're different due to Windows version.  This structure is (and many other APIs are) extended as OS versions get released - new items get tacked on the end.  Here's a list, I'm sure you can find more:
  1. On MSDN, the Access 2003 VBA Language Reference for PrtDevMode
  2. On MSDN, the Win32 API for DEVMODE
  3. On MS Support, KB 282474: How To Print Using Custom Page Sizes on Windows NT and Windows 2000
  4. On MS Support, KB 190218: How To Retrieve Settings From a Printer Driver
  5. On MSDN, the VC++ 6 DEVMODE
  6. On MSDN, the Windows Mobile 6 DEVMODE
  7. On MSDN, the Windows CE 5.0 DEVMODE
They may all look the same on the surface, but no...
First, confusion sets in when I read article #1 which says PrtDevMode "is a 94-byte structure that mirrors the DEVMODE structure defined in the Win32 Software Development Kit," yet if I try to add up the byte-counts of the structure elements (ints = 2 bytes, longs = 4) I easily arrive at 108 bytes.
I say "that can't be right, let's see what the Win32 API says" so off I go to read article #2.  Despite the definitions for BCHAR, DWORD, POINTL, etc... being unlinked (I eventually track them down) I find that this structure totals 126 bytes.  (Don't let the "union" fool you - it's a C language element that is an "either-or.")  And the "FormName" element here says 32 bytes, but article #1 says 16 characters?  Oh, it must be using Unicode...
Ahhh, right.  So I say, "Let's go find a tie-breaker - maybe something with a concrete example to work with," and find article #3.  Wait... "FormName" is now shown as 32 characters.  And the element right after that is an Integer, not a Long.
I can feel the quicksand creeping up over my chin.
For most people (I have to assume) these details don't matter, as they're typically interested in the earlier elements of the DEVMODE structure like the Orientation - so having the structure misaligned or even completely hosed later on is of no consequence.  In my case, however, I have to manage the Media Type - which isn't shown on most of those articles (it's the dmMediaType element on article #2 - it's a later addition to the Win32 API).  This means I need to be EXTREMELY sure that everything is lined up right, or I'll crash Access intermittently if I'm unlucky, or regularly if I'm lucky.  (I wasn't lucky.  I thought I'd found the right struct, but it was wrong, and caused Access to GPF from time to time while printing - just not while it went through QA.)
#4 Access and VBA Suck at Structs
I'm getting pretty blunt now, aren't I?  Sorry.  Well, not really.
In conjunction with the problem of actually finding the correct structure for DEVMODE, I had to get that structure pasted into the variant/string/byte-array PrtDevMode property of the Access Report.  "Converting" structures and byte-arrays into strings isn't a straightforward operation in VBA.  Let's face it - if you have to do that kind of thing, you shouldn't be doing it in VBA.  Ouch.  Point taken.  You should know I'm doing this under duress...
Microsoft knows this fact, that's why they stored the DEVMODE structure inside the Report object as a string.  That, and they probably didn't want you to change it either.
The Technical Solution
Solving those technical problem(s) to arrive at a business solution requires two separate (yet related) parts.  The first is to apply a known set of "adjustments" (specify a printer, a page orientation, tray, paper size, media type) to an existing report such that it prints to the desired printer using the right tray and paper type.  The second part is to make the configuration of those printer settings relatively painless by allowing an "administator" to easily extract the printer settings from a test report that's been configured by using the standard Page Setup dialog.
Part 1: Applying a Printer Configuration
The code sample on my SkyDrive has a function called SetReportPrinterProperties that applies specific printer settings to a given report - open that up to illustrate the discussion that follows.
The basic idea of this function is that we're going to retrieve a "known good" version of the DEVMODE contents directly from the printer driver, then make changes to its configuration, and save those changes to the report.  This process does not:
  • Change the user's default printer.
  • Change the settings for the user's default printer.
  • Change the "default" settings for the desired printer.
We solve the technical problems numbered above by:
  1. Sidestepping the partially implemented MS Access printer setting control with one that uses the "complete" Win32 API DEVMODE structure.  This allows us to alter settings like Media Type.
  2. Providing an interface to apply ANY printer settings to ANY report at ANY time without altering the default settings for any printer, or changing the user's default printer.
  3. Finding a correct DEVMODE structure (at least for Windows Server 2003!) by trial and error.  The winner?  Link #5... plus "extra items" from link #2.
  4. To get a byte array (from a Win32 API call to DocumentProperties) stuffed into a VBA DEVMODE structure: a CopyMemory Win32 API call.  To get a VBA DEVMODE structure stuffed into the Report's PrtDevMode variant/string/byte-array: An LSET to a "string struct" followed by an extraction of the string.
Part 2: Extracting a Printer Configuration
This is where the GetReportPrinterProperties function comes in from the sample.  Give a regular user any Access report you like, and have them set it up using the standard Page Setup dialog until it's "just right".  Once that's done, you can use this function to extract their settings from the stored DEVMODE structure in the Report object.  This extraction function is a lot less complex, because we're only reading info, not reading, changing, casting, and writing.
Conclusion
Do I have to say it?  OK.  Don't use MS Access for anything except prototyping or simple, single-user, single-purpose apps.  Right now, I'm crossing my fingers that this gets through an extended QA process - since my last flawed attempt snuck through the regular QA.
If you have to extend the DEVMODE usage to some other properties that have been added to later versions of Windows, I'm hoping all you have to do is add those elements to the end of the "udtDevMode" type I've got in the code sample.
If anyone has any other pointers about how to handle this better, or knows of anything that will come to bite me later - please say so!  I have my fingers crossed that this is the last I'll be dealing with this, so I can get more quality time with my Guerilla BI project...

Monday, February 2, 2009

SSAS Quick Reference: Attribute Key Cannot Be Found

This is a reference post for me, mostly - I keep forgetting this stuff about SSAS... particularly cause #2.
Error
Errors in the OLAP storage engine: The attribute key cannot be found when processing: Table: [table name], Column: [column name], Value: [value]. The attribute is [attribute name].
Explanation
This means SSAS could not find a record in the dimension table [table name] where column [column name] contained value [value].  Essentially, a "SELECT COUNT(*) FROM [table name] WHERE [column name] = [value]" returned zero.
Cause(s) and Solution(s)
#1 - Errors in ETL
This doesn't happen to me - but is apparently most the common cause: You've loaded facts into the fact table, but haven't loaded the requisite dimension members into the dimension table.  (This may be an ETL design error, or a true data error.)  This can ONLY happen if you're not using surrogate keys to map your facts to your dimensions.  (Which is why it can't happen to me.  If you're using surrogate keys, one of your steps in processing the fact data is to look up the surrogate keys - where you'll end up "not finding" one, and have to deal with it there, in your ETL, instead of in cube processing.)
==> Load your dimensions properly.  If you can rearchitect your warehouse to use surrogate keys for dimension members - DO IT.
#2 - Bad Processing Order
This is (apparently) fairly typical.  You may have instructed SSAS to process a Fact table (measure group) before the dimensions it references have been processed.  For example, you've run an ETL process to update your RDBMS warehouse.  The ETL added some new dimension members, and new facts which refer to those new dimension members.  If you process the measure group first, it will try to look up the new dimension keys in the "old" dimension.
==> Process the dimension(s), then the cube.
#3 - NULLs in Your Fact or Dimension Data
Apparently also common due to (IMO) bad warehousing design. 
If you have a NULL value in one of your Fact table keys, SSAS may report that it can not locate a default valued key in the dimension table.  To restate, if an integer type foreign key in the fact table is valued as NULL, SSAS may report it can't locate "0" in the Dimension table. 
If you have a NULL value in one of your Dimension table attributes, SSAS may be unable to find an attribute key in a "snowflake" type warehouse due to rows being excluded because they don't join up to the NULL.  The worst part is that the NULL valued attribute may not be related (in an obvious manner) to the attribute it reports as being unable to be found...
==> Check for NULL values in your Fact tables and Dimension tables.  If you can rearchitect your data warehouse to eliminate all NULL values - DO IT.  (see Kimball Design Tip #43.)
#4 - Data Collation Inconsistencies
The collation of the dimensional database (or column) could be incorrect.  Your RDBMS warehouse could be set to case-insensitivity, but the AS database is set to be case sensitive.  Or it could be accent sensitivity that doesn't match.  Be sure to check individual column collations in the RDBMS too... copying or restoring databases from servers with different collations can cause problems.
==> Change the collation of the RDBMS (server, database, and/or columns) and SSAS to match (case sentitivity and accent sensitivity).
#5 - Materialized Reference Dimensions Processed Incorrectly
If you're using a reference dimension (sometimes called a bridge dimension) in SSAS like the AdventureWorks demo does with the Reseller dimension in order to link it to the Geography dimension, be aware that it doesn't get resolved in Dimension Processing like you might think it should.  Materialized Reference Dimensions are processed in Partition Processing.
For excellent details, see Alberto Ferrari's blog post.
=> Process your Partitions