Total Pageviews

Sunday, December 4, 2011

Six Basics of Preventing Pain in Your ERP Implementation

"Simplicity is the ultimate sophistication." -Leonardo DaVinci

Over the last decade of being involved with customers and implementation partners for ERP projects, I simply cannot overstate the above sentiment - if you keep everything simple, you make an ERP project smooth and constructive.

Let me explain further. I remember hearing multiple horror stories related to heavy solutions like SAP and Oracle including implementation failures, busted budgets, law suits, and other negativity. It is contrary that the ERP project is aimed at easing organization's way of working ends up being another pain itself.

I don't wish to paint a simplistic picture that the ERP journey is necessarily tough and complex. Things have changed with the advent of more user friendly ERP products like Microsoft Dynamics AX 2012. But even with the best tools, if you are involved in an ERP implementation you can avoid repeating the mistakes of those who walked before you by heeding these important points:

  1. Did you check your expectations from the project? Are they realistic and timely? If not, get down to drawing board and write down what you want to achieve. One simple exercise would involve business shareholders discussing their pains before finding a solution. E.g. Is high inventory cost your problem or lack of supply chain visibility bothering your business? Do you get your payments on time and your profitability ratios remains green? Information Technology is definitely an enabler and the journey to implement business solution should start from the business issues.
  2. Once you know your problems, you can begin your journey for solutions. The next step is finding the right solution and the partner. Did your solution partner devote time to exploring your problem further before jumping to conclusions? An have they done similar projects? It's a clear analogy to when you fall sick - you go to a specialist and highlight your pain areas. Would you like to go to a doctor who wouldn't have time to explore your problem, has never treated your illness before, and jumps to prescribing the cure?
  3. Once you select your solution and the partner, please ensure that their team remains in place. You cannot invest time and effort sharing your business problems with one consultant and expect a replacement to have the same understanding. Ensure continuity of the team involved right from start till the end of the project.
  4. Break the project into more and smaller milestones. Create 10 milestones instead of 4 or 5. It will create better control and accountability of project stakeholders. Remember the time management principle of taking realistic and smaller targets and achieving them. It creates positive energy in the implementation teams as the smaller targets are easier to achieve. Plus, celebrate your each milestone to compound this positive energy.
  5. A carrot and stick approach works well. Keep the stick around to push your implementation teams - internal and external team members - to achieve the milestones. Dangle lot of carrots too, you need to create a motivated and committed energy around the project. Did you provide any bonus to project team for putting in extra hours?
  6. Customisations - the most evident hurdle to any ERP success. Did you weigh the risks for any customizations to the product? I think this is one area where customers should definitely meet other existing customers of the same product. How has their experience been with customizing the product? Did you evaluate all the options to the customized approach and their benefits?

Beyond these points, the customer users need to love the solution. It is going to be part of their daily routine of activities. I favor the user centric design of ERP products like AX 2012, since historically most ERPs fail in ‘connecting with the users'. Microsoft's entry into business applications market has changed the rules of the game in the last decade. It has brought more customers into the ERP fold and has introduced more customers to user friendly ERPs.

Simplicity should rule - both in the product and the implementation process!!

by Raman Dhooria, IT Consultant, Microsoft India

http://msdynamicsworld.com/story/six-basics-preventing-pain-your-erp-implementation

Sunday, October 16, 2011

Dennis Ritchie: The Shoulders Steve Jobs Stood On


Dennis Ritchie (standing) and Ken Thompson at a PDP-11 in 1972. (Photo: Courtesy of Bell Labs)


The tributes to Dennis Ritchie won’t match the river of praise that spilled out over the web after the death of Steve Jobs. But they should.


And then some.

“When Steve Jobs died last week, there was a huge outcry, and that was very moving and justified. But Dennis had a bigger effect, and the public doesn’t even know who he is,” says Rob Pike, the programming legend and current Googler who spent 20 years working across the hall from Ritchie at the famed Bell Labs.
On Wednesday evening, with a post to Google+, Pike announced that Ritchie had died at his home in New Jersey over the weekend after a long illness, and though the response from hardcore techies was immense, the collective eulogy from the web at large doesn’t quite do justice to Ritchie’s sweeping influence on the modern world. Dennis Ritchie is the father of the C programming language, and with fellow Bell Labs researcher Ken Thompson, he used C to build UNIX, the operating system that so much of the world is built on — including the Apple empire overseen by Steve Jobs.

“Pretty much everything on the web uses those two things: C and UNIX,” Pike tells Wired. “The browsers are written in C. The UNIX kernel — that pretty much the entire Internet runs on — is written in C. Web servers are written in C, and if they’re not, they’re written in Java or C++, which are C derivatives, or Python or Ruby, which are implemented in C. And all of the network hardware running these programs I can almost guarantee were written in C.

“It’s really hard to overstate how much of the modern information economy is built on the work Dennis did.”
Even Windows was once written in C, he adds, and UNIX underpins both Mac OS X, Apple’s desktop operating system, and iOS, which runs the iPhone and the iPad. “Jobs was the king of the visible, and Ritchie is the king of what is largely invisible,” says Martin Rinard, professor of electrical engineering and computer science at MIT and a member of the Computer Science and Artificial Intelligence Laboratory.
“Jobs’ genius is that he builds these products that people really like to use because he has taste and can build things that people really find compelling. Ritchie built things that technologists were able to use to build core infrastructure that people don’t necessarily see much anymore, but they use everyday.”

From B to C
Dennis Ritchie built C because he and Ken Thompson needed a better way to build UNIX. The original UNIX kernel was written in assembly language, but they soon decided they needed a “higher level” language, something that would give them more control over all the data that spanned the OS. Around 1970, they tried building a second version with Fortran, but this didn’t quite cut it, and Ritchie proposed a new language based on a Thompson creation known as B.

Depending on which legend you believe, B was named either for Thompson’s wife Bonnie or BCPL, a language developed at Cambridge in the mid-60s. Whatever the case, B begat C.
B was an interpreted language — meaning it was executed by an intermediate piece of software running atop a CPU — but C was a compiled language. It was translated into machine code, and then directly executed on the CPU. But in those days, C was considered a high-level language. It would give Ritchie and Thompson the flexibility they needed, but at the same time, it would be fast.

That first version of the language wasn’t all that different from C as we know it today — though it was a tad simpler. It offered full data structures and “types” for defining variables, and this is what Richie and Thompson used to build their new UNIX kernel. “They built C to write a program,” says Pike, who would join Bell Labs 10 years later. “And the program they wanted to write was the UNIX kernel.”

Ritchie’s running joke was that C had “the power of assembly language and the convenience of … assembly language.” In other words, he acknowledged that C was a less-than-gorgeous creation that still ran very close to the hardware. Today, it’s considered a low-level language, not high. But Ritchie’s joke didn’t quite do justice to the new language. In offering true data structures, it operated at a level that was just high enough.

“When you’re writing a large program — and that’s what UNIX was — you have to manage the interactions between all sorts of different components: all the users, the file system, the disks, the program execution, and in order to manage that effectively, you need to have a good representation of the information you’re working with. That’s what we call data structures,” Pike says.

“To write a kernel without a data structure and have it be as consist and graceful as UNIX would have been a much, much harder challenge. They needed a way to group all that data together, and they didn’t have that with Fortran.”

At the time, it was an unusual way to write an operating system, and this is what allowed Ritchie and Thompson to eventually imagine porting the OS to other platforms, which they did in the late 70s. “That opened the floodgates for UNIX running everywhere,” Pike says. “It was all made possible by C.”


Apple, Microsoft, and Beyond
At the same time, C forged its own way in the world, moving from Bell Labs to the world’s universities and to Microsoft, the breakout software company of the 1980s. “The development of the C programming language was a huge step forward and was the right middle ground … C struck exactly the right balance, to let you write at a high level and be much more productive, but when you needed to, you could control exactly what happened,” says Bill Dally, chief scientist of NVIDIA and Bell Professor of Engineering at Stanford. “[It] set the tone for the way that programming was done for several decades.”

As Pike points out, the data structures that Richie built into C eventually gave rise to the object-oriented paradigm used by modern languages such as C++ and Java.
The revolution began in 1973, when Ritchie published his research paper on the language, and five years later, he and colleague Brian Kernighan released the definitive C book: The C Programming Language. Kernighan had written the early tutorials for the language, and at some point, he “twisted Dennis’ arm” into writing a book with him.

Pike read the book while still an undergraduate at the University of Toronto, picking it up one afternoon while heading home for a sick day. “That reference manual is a model of clarity and readability compared to latter manuals. It is justifiably a classic,” he says. “I read it while sick in bed, and it made me forget that I was sick.”

Like many university students, Pike had already started using the language. It had spread across college campuses because Bell Labs started giving away the UNIX source code. Among so many other things, the operating system gave rise to the modern open source movement. Pike isn’t overstating it when says the influence of Ritchie’s work can’t be overstated, and though Ritchie received the Turing Award in 1983 and the National Medal of Technology in 1998, he still hasn’t gotten his due.

As Kernighan and Pike describe him, Ritchie was an unusually private person. “I worked across the hall from him for more than 20 years, and yet I feel like a don’t knew him all that well,” Pike says. But this doesn’t quite explain his low profile. Steve Jobs was a private person, but his insistence on privacy only fueled the cult of personality that surrounded him.

Ritchie lived in a very different time and worked in a very different environment than someone like Jobs. It only makes sense that he wouldn’t get his due. But those who matter understand the mark he left. “There’s that line from Newton about standing on the shoulders of giants,” says Kernighan. “We’re all standing on Dennis’ shoulders.”


Additional reporting by Jon Stokes.

Thursday, October 13, 2011

Steve Jobs: The Wilderness, 1985-1997

Steve Jobs: The Wilderness, 1985-1997
Cast out from Apple, Jobs tried—and failed—to make a different kind of computer

Steve Jobs: The Beginning, 1955-1985

Steve Jobs: The Beginning, 1955-1985
The high school loner who figured out what the world wanted from technology

The Google executive chairman admired Jobs's passion, courage, and smarts

Eric Schmidt on Steve Jobs
The Google executive chairman admired Jobs's passion, courage, and smarts
Jobs and Schmidt connect at the introduction of the iPhone, 2007 Kim Kulish/Corbis

Everyone knows the transaction where the board sided with John Sculley and Steve left Apple (AAPL). Steve sold all of his Apple stock, kept one share, and founded NeXT. Typical Steve maneuver. When I was still at Sun Microsystems, I visited him at NeXT—we did a bunch of deals with him. He was exactly the same way he was at Apple: strongly opinionated, knew what he was doing. He was so passionate about object-oriented programming. He had this extraordinary depth. I have a PhD in this area, and he was so charismatic he could convince me of things I didn’t actually believe.

I should tell you this story. We’re in a meeting at NeXT, before Steve went back to Apple. I’ve got my chief scientist. After the meeting, we leave and try to unravel the argument to figure out where Steve was wrong—because he was obviously wrong. And we couldn’t do it. We’re standing in the parking lot. He sees us from his office, and he comes back out to argue with us some more. It was over a technical issue involving Objective C, a computer language. Why he would care about this was beyond me. I’ve never seen that kind of passion.

At NeXT he built this platform—a powerful workstation platform for the kind of computing that I was doing, enterprise computing. When he came back to Apple, he was able to take the technology he invented at NeXT and sort of slide it underneath the Mac platform. So today, if I dig deep inside my Mac, I can find all of that NeXT technology. Now, this may not be of interest to users, but without the ability to do that the Mac would have died. I was surprised that he was able to do that. But he did it.

When he went to Apple, he was basically down to 1 percent market share. Apple was near bankruptcy, the company had been for sale, there were a series of management changes. I talked to him about it. He said, “The thing that I have that no one else has is very loyal customers.” He had these fanatical people who would line up all night for a product that wasn’t any good. He figured correctly that by upgrading and investing in and broadening the portfolio, he could do it. At some level he foresaw the next 10 years.

What I remember thinking at the time is that you shouldn’t take a job unless you know how to win. I had no clue how to do what he did. When somebody tells you they’re going to do something and you say, “I don’t understand how you’re going to do that,” and they succeed? That is the ultimate humbling experience. My interactions with Steve were always like that. He was always ahead of me. When he started working on tablets, I said nobody really likes tablets. The tablets that existed were just not very good. Steve said: “No, we can build one.” One of the things about Steve is, he was always in the realm of possibility. There was a set of assumptions that Steve would make that were never crazy. They were just ahead of me.

I joined Apple’s board after the Apple Stores started. It used to be that you would go to a store and you had Macs and PCs. And then, because of volume and because of the Microsoft (MSFT) monopoly, people were not buying any Macs. There was less and less distribution, and many dual Mac-PC distributors were going away. The argument at the time was you shouldn’t screw your distributors because they are your lifeline. Steve made the calculated decision to open a series of stores and turn it into a sort of a consumer lifestyle. He also understood that people had trouble with computers, and they wanted to go to a place where somebody could help them. The stores were universally derided as the stupidest idea ever known to man, and they would literally bankrupt the company. It was an incredibly gutsy move. And Apple Stores I believe are the highest-grossing stores in America.

It took enormous courage for Steve to go through the operations, the treatments—without violating his privacy, it’s just horrific what he had to go through. I think he made all the board meetings I was at. He was obviously ill sometimes, and sometimes he was fine. But Apple was his passion, along with his family. There was never any question when I was there as to his ability to do his job, and I just felt terribly sorry for him, as everyone else did, over what he was going through physically.

Steve and I were talking about children one time, and he said the problem with children is that they carry your heart with them. The exact phrase was, “It’s your heart running around outside your body.” That’s a Steve Jobs quote. He had a level of perception about feelings and emotions that was far beyond anything I’ve met in my entire life. His legacy will last for many years, through people he’s trained and people he’s influenced. But what death means is you can’t call—you can’t call him. It’s a loss. I’ll miss talking to him.

— As told to Jim Aley and Brad Wieners

Tuesday, August 9, 2011

Effective Management: 5 Critical Skill Areas


Managing effectively is not just one skill, but a mix of different skills. It is a combination of different kinds of intelligence we have as human beings, which makes it an art and a craft.
Have you seen a manager who is highly skilled in technical areas but lacks empathy for others? Or the one who is highly people oriented, but easily loses the sight of goals?
If you are a manager at any level in the organization (or an aspiring one), here are some of the most critical skills you should work on.
Technical Expertise: Broad understanding of the subject (meta-cognition), various components involved in getting work done, links between those components, technical awareness and problem solving skills.
Analytical Intelligence: Ability to gather facts, understand the goals in numbers, compile data into information, measure, see trends, predict the outcomes, go to the root cause and base decisions on facts.
People Intelligence: Understand people (and how they feel), practice empathy, motivate them, align them to the goals, coach and mentor, create a positive influence, understand inter-personal dynamics, communicate (and connect) and understand verbal/non-verbal communication.
Operational Intelligence: Ability to define work as series of interconnected actions, detailed planning, constant alignment of process, improving, seeing waste (and eliminating it), provide a process platform to teams, define rituals, review everything, provide clarity and manage expectations.
‘Big Picture’ Thinking: Ability to see the larger picture (the whole) and visualize its parts, visualize impacts of change, identify new possibilities, align ideas to the larger goal, identify/foresee required changes/trends, define the future, communicate the vision, experiment and be comfortable with ambiguity.
Source: http://qaspire.com/2011/07/25/effective-management-5-critical-skill-areas/

Wednesday, August 3, 2011

Uncle Sam's first CIO

How batch processing works under the hood AX2009


In this article I am going to explain how batch processing in AX2009 works, I don't mean how to set up a batch group or any of that kind of thing that you find in the manual, what I mean is what each AOS is doing in the background to decide how and when to pick up batches and process and complete them. Understanding this background can help in advanced batch troubleshooting or development scenarios.
In AX2009 batch processing changed. Now we have AOSes which can run batch processes directly, if you want to see what's happening with a batch process, it can be more difficult than in AX3 or AX4 as there is no client sitting there running to look at.
What happens now is that each AOS has a dedicated thread which checks for batches, basically all this does is calls Classes\BatchRun.ServerGetTask() once every 60 seconds (timing is not configurable) and if there is any work for that AOS to do then the AOS will pick up a task from here.
I'll give an example of an end-to-end batch process to show what happens where and when:
- A report is sent to batch by a user, it goes into the batch queue in BATCHJOB (header) and BATCH (the batch tasks).
- Once every 60 seconds each AOS that has been configured for batch processing (in administration->setup->server configuration) will call the X++ method - Classes\BatchRun.serverGetTask()
- In serverGetTask() the logic is exposed in X++ so we can all see what happens, this is the main place that we decide what to pick up for batch processing. Basically it checks if there is any tasks in the BATCH table waiting for this AOS - based on the batch groups that this AOS is configured to process, and based on the time that the records in BATCH are due to be processed (i.e. something processes at 21:00 each day then it won't get picked up until 21:00 despite the fact that the AOS polls every 60 seconds). There are a few stages to this method:
 1. First we check if there is a task (a task is a record in BATCH table) ready for us, the query for this is like this:
select firstonly pessimisticlock RecId, CreatedBy, ExecutedBy, StartDateTime, Status,
        SessionIdx,SessionLoginDateTime, Company, ServerId, Info
    from batch
    where batch.Status == BatchStatus::Ready
    && batch.RunType == BatchRunType::Server
    && (Session::isServer() || batch.CreatedBy == user)
    join Language from userInfo
        where userInfo.Id == batch.CreatedBy
        && userInfo.Enable == true
    exists join batchServerGroup
        where batchServerGroup.ServerId == serverId
            && batch.GroupId == batchServerGroup.GroupId;
 2. If a task is returned in step 1 then there's nothing more to do and we start processing that task. If no task is returned then we look to see if any batch jobs need to be started, the query for this is like this:
 update_recordset batchJob setting
                Status = BatchStatus::Executing,
                StartDateTime = thisDate
            where batchJob.Status == BatchStatus::Waiting
                &&  batchJob.OrigStartDateTime   <= thisDate
            exists join batch
                where batch.BatchJobId == batchJob.RecId
            exists join batchServerGroup
                where batch.GroupId == batchServerGroup.GroupId
                && batchServerGroup.ServerId == serverId;

 
3. After step 2 we will run Classes\batchRun.serverProcessDependencies(). In here something interesting happens - we see that we use this table "BatchGlobal", this is used as a focal point, because we might have several AOSes running batch processing in the same environment, and so for some operations we look to this table to see if another AOS has already done something, to decide whether the current AOS needs to do it as well or not. For dependencies we just make sure that another AOS is not doing this in the same second. So if we continue here, the queries we run to set more tasks (again tasks are just records in the BATCH table) ready for processing are below - you can see in the queries how we update the status on the BATCH table records, checking that we only do it for records which are ready and do not have any constraints that are not completed yet:
   
//There are no more available tasks and the user is asking for any task. Search for more tasks with
    //dependencies
    update_recordset batch setting Status = BatchStatus::Ready
    where batch.Status == BatchStatus::Waiting
        && batch.ConstraintType == BatchConstraintType::Or
    exists join batchJob
        where batchJob.Status == BatchStatus::Executing
            && batch.BatchJobId == batchJob.RecId
    exists join constraintsOr
        where constraintsOr.BatchId == batch.RecId
    exists join batchDependsOr
      where
     (
        ((batchDependsOr.Status == BatchStatus::Finished
            && (constraintsOr.ExpectedStatus == BatchDependencyStatus::Finished
                || constraintsOr.ExpectedStatus == BatchDependencyStatus::FinishedOrError))
        || (batchDependsOr.Status == BatchStatus::Error
            && (constraintsOr.ExpectedStatus == BatchDependencyStatus::Error
                || constraintsOr.ExpectedStatus == BatchDependencyStatus::FinishedOrError)))
        && constraintsOr.DependsOnBatchId == batchDependsOr.RecId
     );

    update_recordset batch setting Status = BatchStatus::Ready
    where batch.Status == BatchStatus::Waiting
        && batch.ConstraintType == BatchConstraintType::And
    exists join batchJob
        where batchJob.Status == BatchStatus::Executing
            && batch.BatchJobId == batchJob.RecId
    notexists join constraintsAnd exists join batchDependsAnd
    where
     (
        constraintsAnd.DependsOnBatchId == batchDependsAnd.RecId
        && constraintsAnd.BatchId == batch.RecId
        && ((batchDependsAnd.Status != BatchStatus::Finished && batchDependsAnd.Status != BatchStatus::Error)
            || (constraintsAnd.ExpectedStatus == BatchDependencyStatus::Finished
                && batchDependsAnd.Status == BatchStatus::Error)
                || (constraintsAnd.ExpectedStatus == BatchDependencyStatus::Error
                && batchDependsAnd.Status == BatchStatus::Finished))
     );

4. When this serverProcessDependencies() is complete in step 3 we call again to serverGetOneTask() (same as in step 1), if there were some more tasks set to "ready" in step 3 then we might pick up a task to work on here. Of course if not tasks were "ready" in step 3 then we won't find a task and we'll just do nothing.
- So our report which we sent to batch, if in the steps numbered 1-4 above, we found this record was ready to process, and we picked it up, what happens next inside the AOS kernel is that we start a worker session, which can be thought of a bit like a client session, just without a client, it will have it's own session ID and you'll see the ID recorded against the record in the Batch table. From this point it calls BatchRun.runJobStatic() and actually runs the batch process - this is just normal X++ running the process here. When this runJobStatic() completes we call BatchRun.ServerFinishTask(), which just sets the status of the record in BATCH to either "finished" or "error" (if it failed for some reason).
- Now our batch task is finished - the record in the BATCH table. But the header for this batch, the Tables\BatchJob record is not set to finished yet. For this part there is another background process running every 60 seconds on each AOS which just calls into BatchRun.serverProcessFinishedJobs(). Now we can see in this X++ method what it does - we use this BatchGlobal table again, to make sure that between all AOSes we only check for finished jobs a maximum of once every 60 seconds, if it has been 60 seconds then we will run a whole load of queries (too many to copy here but you can check there to see them) to create the batch history (various tables), set the BatchJob record to finished and delete the completed tasks and constraints.
There are a couple of other background things that happen in the AOS kernel for batch processing:
1. Every 5 minutes it will call to BatchRun.serverCleanUpDeadTasks() - again we use the BatchGlobal table, so that we'll only run this once every 5 minutes between all AOSes. This just sets tasks back to "ready" if the session ID for the worker session (I mentioned this earlier - we create this worker session when we start processing a task) is no longer a valid session - basically if a task fails with an X++ exception, or something like that, then the worker session will end, and if you have configured this batch task to allow some retries, then it's this method which will reset the task for it to have a retry.
2. Every 5 minutes each AOS will check the server settings, to see if it's supposed to process the same batch groups - or if it's not supposed to be a batch server any more, all those settings.

Tuesday, August 2, 2011

Create your own shortcuts in AX forms

In AX it is virtually impossible to control the shortcuts of buttons. Even though button properties like KeyTip indicate that you should be able to have some control, nothing really works.
Here is a tip on how you can catch special keyboard combinations yourself, and thus create your own shortcuts.

First you need to call a method from USER32.DLL to figure out if a certain key is pressed. You could for example implement this on the WinAPI class:




static boolean getKeyPressed(int _keyCode)
{
    DLL winApiDLL = new DLL('USER32');
    DLLFunction getKeyState = new DLLFunction(winApiDLL, 'GetAsyncKeyState');
    int result;
    ;

    getKeyState.returns(ExtTypes::WORD);
    getKeyState.arg(ExtTypes::DWORD);

    result = getKeyState.call(_keyCode);

    if ((result & 0x8000) == 0x8000)
        return true;
    else
        return false;
}



The method is called with what’s called a virtual key code, which is the code of the key you want to know is pressed or not. You can find a list of these keyboard codes on MSDN: http://msdn.microsoft.com/en-us/library/dd375731(VS.85).aspx

On your form you need to implement a method that is executed again and again while the form is idle. The method needs to check if your special key combination is pressed, do the action associated with the key and set the method up for next iteration.
It could look like this:

void myKeyboardCheck()
{
    #define.ALT(0x12)
    #define.O(0x4F)
    #define.timeOut(10)
    ;

    // Check if this form is the foreground window of Windows
    if (winApi::getForegroundWindow() == this.hWnd())
    {
        // Check if ALT + O is pressed
        if (WinApi::getKeyPressed(#ALT) && WinApi::getKeyPressed(#O))
        {
            // Check if the button is enable
d            if (MyVerySpecialButton.enabled())
            {
                MyVerySpecialButton.clicked();
            }
        }
    }
    // Reset timer
    this.setTimeOut('myKeyboardCheck', #timeOut, true);
}

In the run method of the form, I set the above method up for the first iteration, and I change the label of the button to reflect the desired shortcut:

MyVerySpecialButton.text(MyVerySpecialButton.labelText() +' (o)');
this.myKeyboardCheck();

If you need to implemt this in more forms, you might want to build a small supporting framework in order to avoid repeating much of the same code over and over. The class DocuFileWatchDog could be a good pattern to look at.

If you need to catch keyboard combinations at a global level, you should hook into the Info.OnEventGoingIdle() method. Global reserved keys are defined under \Application Documentation\Global\Shortcutkeys.

I want to give a big thank you to Microsoft support for pointing me in the right direction. I can only wonder why Microsoft haven't gotten this stuff correctly wired up in the first place.