In nearly every bit of WPF training material or weblog there is some reference to the "ah-ha" moment. The point at which all the new concepts you've been learning gel into a cohesive unit.
For me, that moment was one or two months after I started developing WPF in earnest (by which I mean for users other than myself).
The task was to display two different indicators of a business metric, a green up arrow and a red down arrow.
My first instinct was to draw the two different arrows, export images to PNG, embed them as resources, and then raise an event each time it changed so that some code behind could dynamically show the appropriate image.
So tedious and so not the way to do it.
The first realization if there is no image - there is only a shape, or more specifically a Path.
<Path Data="M8,40L8,24 0,24 16,0 32,24 24,24 24,40z" Stroke="Black" />
That produced the outline of the arrow.
The next realization was that I could use data binding on Shape properties such as Fill and RotateTransform. My previous mental map around data binding was that it was limited to direct business data, by breaking that model, whole new code-expressive opportunities opened up. And by binding to these newfangled dependency properties - the shape would be updated whenever the properties changed.
The first attempt still was too much code, as I assumed I needed a value converter
Fill="{Binding Path=BizMetric, Converter={StaticResource BizMetricToBrushConverter}}"Again, too tedious and not the way to do it.
My final realization resulted in the code that's in use today, with dependency properties of the type needed directly on the presenter class, and data-bound to the shape
<Path
Data="M8,40L8,24 0,24 16,0 32,24 24,24 24,40z"
Fill="{Binding Path=BizMetricFillBrush}"
RenderTransformOrigin="0.5,0.5" >
<Path.RenderTransform>
<RotateTransform Angle="{Binding Path=BizMetricRotationAngle}"/>
</Path.RenderTransform>
</Path>
Update:
Russell Mull suggests the use of DataTriggers - which indeed, let you do all the styling in xaml.
<Style TargetType="{x:Type Path}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=BizMetricDirection}" Value="1">
<Setter Property="Fill" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=BizMetricDirection}" Value="0">
<Setter Property="Fill" Value="Transparent" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=BizMetricDirection}" Value="-1">
<Setter Property="Fill" Value="Red" />
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="180"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
What was your "ah-ha" moment with WPF?

Deployment is one of the software project taxes that are often neglected or shunted aside to have another team deal with.
There are two big downsides when a team does not directly own it's deployment. The first is obvious, if the quality of the deployment infrastructure isn't any good, users perception of your software will suffer. Rightfully so, any you find yourself spending time fixing bugs that are only tangential to your project.
The second risk has to do with how outsourcing (to another group, another department or other company) deployment infrastructure can affect your project. If the amount of time it takes to get a new deployment ready is more than an hour - you'll find yourself adjusting the release schedule to accommodate this - typically in the direction of longer delays between releases. No need to rehash the downside of that.
Here are some tips to taking control of deployment.
Secret #1
To install to the desktop, use ClickOnce.
You no longer need to be dependent on the installation and packaging department.
The core ClickOnce technology continues to improve from its inception in .Net 2.0 - In .Net 3.5 SP1 ClickOnce has support for the reduced client profile, FireFox installs, and file type registration.
For enterprise environments there are a few things that you'll need/want to do to make ClickOnce really work for you
The most important thing is having support for multiple environments - this isn't built it, and if you attempt to deploy two different ClickOnce builds with the same deployment name to different sites, the latest build will take precedence and effectively overwrite the existing deployment on the desktop.
The fix for this is relatively straightforward - you need to provide different deployment name for each build. Like so -
<MSBuild
Projects="ClickOnce.csproj"
Targets="Publish"
Properties="
MinimumRequiredVersion=$(MinimumRequiredVersion);
ApplicationVersion=$(ApplicationVersion);
ApplicationRevision=$(ApplicationRevision);
CodeBranch=$(CodeBranch);
DeployEnv=$(DeployEnv)
AssemblyName=ClickOnce.$(DeployEnv);
PublishUrl=$(PublishUrl);
ProductName=ClickOnce $(CodeBranch) $(DeployEnv)" />
The one limitation of this approach is that project references will no longer work. Use file based assembly refs, and it'll be fine.
For a more polished look obtain a code signing certificate. Instead of getting an install dialog that says "Unknown publisher", you can indicate the name of your group. It also make the auditors feel warm and fuzzy.
And finally, you can make your desktop software URL addressable. Even though most workflow software in the enterprise has moved to the web, the need for desktops software still exists, and often this desktop software can benefit by supporting URLs. And rather than telling your colleague how to navigate to what you're looking at, or sending a screen shot of what you're looking at, you could send them a URL - which would open up to the screen you're using.
If you were installing the software via windows installer you could rely on modifying the registry and register a protocol handler. That approach won't work for non-administrator ClickOnce installs, but you can work around that limitation by enabling "URL parameters to be passed to the application" (thanks to my co-worker Eugene for discovering this one)
Update - Cedric in the comments wonders if ClickOnce limits the application to a ".Net Sandbox?" In many ways, not really. In full trust mode, ClickOnce applications can run unmanaged code via pinvoke and Reg-Free COM. For applications that truly need unmanaged component installs you can use the setup bootstrapper, or if you are in an enterprise situation, install the core components separately from the application.
Secret #2
To get the server components and ClickOnce files to the deployment site, use WiX to prepare a Windows Installer packages.
Sure, you could use Visual Studio Setup Projects, or even shell out some real money for InstallShield or Wise software. But WiX has a number of nice features
- cost
- text (well xml) based files so you can do the decent diffs and source control on them
- active community support
- reasonable learning curve - start with everyone's favorite WiX tutorial
Secret #3
Get your configuration files under control
Secret #4
Although WiX is great, sometimes installs just too difficult to be done in Windows Installer technology. In this case, use PowerShell.
If the install involves modifying xml files, executing database commands, or invoking web services and it's well outside the normal range for windows installer. Read than trying to force everything into one technology, simply distribute a PowerShell setup scripts in the installer package. Run them from the msi or by hand afterwards.
Secret #5
Working on deployment can help your career.
It's true. Responsibility and influence on a project naturally flow to those that get things done (not sure if it matter that you're smart or not). Few things are a more visible indication getting things done then a release, and if you own deployment, other project items will naturally flow your way.
Typically during an overview of some new technology they will say "and it's totally configurable" at which point my inner sarcastic voice pipes up in response "great, now we have two problems".
Configuration files are getting bigger and more complex. In some ways this is a good thing as there's a move towards a declarative approach, but in other ways it's unneeded as the cost of compilation has gone down. The real problems with the move towards configurable everything is the configuration files often lack designer support, and do not get verified at compile time.
I can't help with the lack of designers or verification, but I do have some solutions for managing the proliferation of config files and multiple environments.
In a typical enterprise development project, you may have as many as five environments to execute against.
- localDev - the PC you develop and unit test on
- sharedDev - a shared environment with minimal expectations around quality
- UAT/QA - a shared an environment with explicit quality expectations - typically this means that developers publish a list of known bugs to testers before the release and the new bugs discovered by testers will be addressed
- Production - this system that users use
- COB/DR - the backup system that users will use "in case of"
I've seen a number of approaches for dealing with configuration files and multiple environments
0. Edit the configuration file by hand right after you deploy.
If this approach works for you, then god bless. You can stop reading this post. On the other hand, if this turns your hair gray, or if you're deploying technology has has tamper protection (such as ClickOnce), then keep reading
1. Put all environmental information into the single configuration file. Typically this approach it is combined with a UI addition allowing the user the option to select which environment to use.
This approach has some merits. Everything is in a single location, so it's easy to find and maintain. On the other hand this approach makes it way too easy to run un-tested development code against a production environment, and doesn't completely solve the configuration problem - and that it doesn't tell a server component what environment it's currently running in.
2. Keep 5 separate configuration files, one for each environment.
This approach looks the DRY principle in the eye, and says " what are you going to do about it?" It's fine if you're feeling tough, and in the mood for a maintenance headache. For example if you add a new logging sink, you need to edit 5 files.
3. Treat your configuration files as code to be compiled
Benefits: no duplication of effort, automated, safe, unambiguous, auditable.
Cons: Takes about 1 developer day to setup.
Here's how it works. The configuration files for each assembly are always kept in localDev configuration. A new file, call it Deployment.Properties is the added of root of the solution. For each assembly, a new file is applied called DeployTransforms.Properties is added.
The Deployment.Properties file is kept in MSBuild format and represents the golden definition of environment settings. It might look something like this
<PropertyGroup Condition="'$(DeployEnv)' == 'sharedDev'">
<AccountsDBServer>accountsdev.company.com</AccountsDBServer>
</PropertyGroup>
<PropertyGroup Condition="'$(DeployEnv)' == 'UAT'">
<AccountsDBServer>accountsUAT.company.com</AccountsDBServer>
</PropertyGroup>
<PropertyGroup Condition="'$(DeployEnv)' == 'Prod'">
<AccountsDBServer>accounts.company.com</AccountsDBServer>
</PropertyGroup>
<PropertyGroup>
<AccountsDBCS>Data Source='$(AccountsDBServer)';Integrated Security=True;</AccountsDBCS>
</PropertyGroup>
The DeployTransforms.Properties file, also kept in MSBuild format defines how the golden values from the Deployment.Properties are applied to the [App|Web].config file.
It might look like this:
<ItemGroup>
<ConfigTransform Include="/configuration/connectionStrings/add[@name='AccountsConnectionString']/@connectionString">
<Value>$(AccountsDBCS)</Value>
</ConfigTransform>
</ItemGroup>
Of course you would add additional properties and config transforms as needed.
The final steps are to include into your project the DeployTransforms.Properties as well as a DeployTransforms target. That might look like this:
<Import Project="ConfigTransform.properties" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="ConfigTransform.targets" />
<Target Name="Publish" DependsOnTargets="$(NonClickOnceDependsOn)">
and ConfigTransform.targets
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<PropertyGroup>
<PublishDependsOn Condition="'$(PublishableProject)'=='true'">
DeployTransform;
$(PublishDependsOn);
RollbackConfigFile;
</PublishDependsOn>
<NonClickOnceDependsOn>
NonClickOnceDeployTransform;
</NonClickOnceDependsOn>
<ConfigFile>App.config</ConfigFile>
<ConfigFileName>$(ConfigFile)</ConfigFileName>
</PropertyGroup>
<Target Name="DeployTransform" >
<Copy SourceFiles="$(ConfigFile)" DestinationFolder="$(IntermediateOutputPath)" />
<Attrib Files="$(ConfigFile)" ReadOnly="false"/>
<CallTarget Targets="UpdateConfigFile" />
</Target>
<Target Name="NonClickOnceDeployTransform" >
<CallTarget Targets="SetConfigFileName" />
<CallTarget Targets="UpdateConfigFile" />
</Target>
<Target Name="SetConfigFileName">
<CreateProperty Value="@(AppConfigWithTargetPath->'$(OutDir)%(TargetPath)')">
<Output TaskParameter="Value" PropertyName="ConfigFileName" />
</CreateProperty>
<CreateProperty Value="$(TeamBuildPublishDir)$(AssemblyName).exe.config" Condition="$(ConfigFileName)=='' and $(TeamBuildPublishDir) !=''">
<Output TaskParameter="Value" PropertyName="ConfigFileName" />
</CreateProperty>
<CreateProperty Value="$(OutputPath)$(AssemblyName).exe.config" Condition="$(ConfigFileName)=='' and $(TeamBuildPublishDir) ==''">
<Output TaskParameter="Value" PropertyName="ConfigFileName" />
</CreateProperty>
<CreateProperty Value="$(TeamBuildPublishDir)\_PublishedWebsites\$(MSBuildProjectName)\Web.config" Condition="$(WebApplication)=='true' and $(TeamBuildPublishDir) !=''">
<Output TaskParameter="Value" PropertyName="ConfigFileName" />
</CreateProperty>
<CreateProperty Value="App.config">
<Output TaskParameter="Value" PropertyName="SrcConfigFileName" />
</CreateProperty>
<CreateProperty Value="Web.config" Condition="$(WebApplication)=='true'">
<Output TaskParameter="Value" PropertyName="SrcConfigFileName" />
</CreateProperty>
</Target>
<Target Name="UpdateConfigFile">
<Error Text="%(ConfigTransform.Identity) has no value for DeployEnv: '$(DeployEnv)'" Condition="'%(ConfigTransform.Value)' == ''" />
<CallTarget Targets="CopyConfigFile"/>
<XmlUpdate
Prefix="n"
Namespace="http://schemas.microsoft.com/developer/msbuild/2003"
XmlFileName="$(ConfigFileName)"
Xpath="%(ConfigTransform.Identity)"
Value="%(ConfigTransform.Value)" />
</Target>
<Target Name="CopyConfigFile"
Outputs="$(ConfigFileName)"
Inputs="ConfigTransform.properties;..\DeployEnv.properties">
<Copy SourceFiles="$(SrcConfigFileName)" DestinationFiles="$(ConfigFileName)" />
</Target>
This approach is invoked via msbuild /t:Publish /p:deployEnv=(UAT|Prod...)
The outputs of the build are now configured for the intended environment.
Every once in awhile you wake up and take notice of a trend that's been bubbling on in the background for some time. For me, today, that trend is the mainstreaming of nontraditional databases.
When I started out in IT choosing a database was easy, it was one of Oracle, Sybase, SQL Server, or Informix. Sometimes DB2 was mentioned.
That's no longer the case. As great as the row-based relational model is, it doesn't work for a number of today's data problems.
- Financial companies use columnar databases for blazing-fast analytic computations
- The use of nontraditional data stores for big web sites is a recurring theme in Dare's blog
- Data now arrives in continuous streams (clicks, ticks, trades, etc) and needs to analyzed and stored appropriately
There are a lot of new products and technologies to support these new data scenarios - two blogs covering this space are:
http://www.dbms2.com/
http://www.databasecolumn.com/
A couple of months ago I put together a quick PowerShell based stream reader for Coral8.
While it was fun to write, beyond demos, it didn't see much use - lack of easy discoverability about which streams were available meant that it was easier to use the Coral8 studio.
So today I have a fix for that, a Coral8 navigation provider. Although it's not nearly fully featured, it's enough to get started.
This implementation takes advantage of the previous PowerShell script to do much of its work. Although it's interesting to see how easy it is to "shell out" to augment the capabilities of c#, I don't see myself doing much more of that style of coding in the future, as debugging the scripts from within C# is too difficult.
Here's how the list of workspaces are obtained. Most of the work is around the raw SOAP calls.
string script = string.Format("([xml]$input).Envelope.Body.{0}Response.return.'#text'",
elementNameNoNS);
return POSHelp.Invoke<string>(script, resp).Single();
public string GetManagerStatus()
{
return GetSOAPResponse("GetManagerStatus", "");
}
public IEnumerable<string> GetWorkspaces()
{
string ms = GetManagerStatus();
return POSHelp.Invoke<string>("([xml]$input).Status.CclWorkspaceInfo.Object | % { $_.Name}", ms);
}
IEnumerable<string> workspaces = C8core.GetWorkspaces();
workspaces.ForEach(wss => WriteItemObject(new WorkSpace() { Name = wss }, path, true));
Getting the list of streams within a project took a little bit more effort, requiring an examination of the workspace status stream
private IEnumerable<string> GetStreamNames(IEnumerable<PSObject> res,string workspace,string program)
{
int markerCount =0;
string match ="/Stream/" + workspace +"/" + program +"/";
Dictionary<string, bool> found = new Dictionary<string, bool>();
var psoTransform = from pso in res select new {
MessageGroup = (string) pso.Properties["MessageGroup"].Value,
MessageName = (string)pso.Properties["MessageName"].Value,
ObjectId = (string)pso.Properties["ObjectId"].Value,
ObjectId2 = (string)pso.Properties["ObjectID2"].Value,
};
foreach (var psoT in psoTransform)
{
if (psoT.MessageGroup =="CclApplicationInfo" && psoT.MessageName =="RunningTime")
{
if (markerCount++ >1)
yield break;
}
if (psoT.MessageGroup =="CclStreamPairInfo") { string[] objecIDPair = { psoT.ObjectId, psoT.ObjectId2 };
foreach (string objid in objecIDPair)
{
var uri = new Uri(objid);
if (uri.PathAndQuery.StartsWith(match) && !found.ContainsKey(uri.PathAndQuery))
{
found[uri.PathAndQuery] = true;
yield return uri.PathAndQuery.Substring(match.Length);
}
}
}
}
}
var urib = new UriBuilder(path.Replace(@"\", "/"));
urib.Path = string.Format("Status/Workspace/{0}", workspace);
IEnumerable<PSObject> res = C8core.OpenStream(urib.ToString());
GetStreamNames(res, workspace, program).ForEach(streamname =>
WriteItemObject(new C8Stream() { Name = streamname }, Path.Combine(path, streamname), true));
And reading the actual stream - in this case, C# is just a pass-through
static string C8StreamReaderScript = @"
($csv,$tblC) = $input | % { $_ }
$vals = $csv.Split(',')
$tblcc = $tblC.Count
if (!($vals.Count -eq $tblcc))
{
throw ""col count error $($vals.Count) -neq $tblcc ""
}
$o = new-object PSObject
for ($ii=0;$ii -lt $vals.Count;$ii++)
{
$t = $tblC[$ii].DataType
if ($t -eq [datetime])
{
$v = ([datetime]'1/1/1970 GMT').AddTicks( ([long]$vals[$ii]) * 10 )
}
else
{
$v = $vals[$ii] -as $t
}
$o | add-member -membertype noteproperty -name $tblC[$ii].ColumnName -value $v
}
$o
";
public IEnumerable<PSObject> OpenStream(string streamUrl)
{
using (WebClient wc = new WebClient())
{
var rurl = ResolveUrl(streamUrl);
DataTable tbl = GetTD(streamUrl);
var req = WebRequest.Create(rurl);
req.Headers.Add("X-C8-StreamFormat", "CSV");
req.Headers.Add("X-C8-StreamFormatOptions", "TitleRow=false");
req.Method = "GET";
using (var resp = req.GetResponse())
using (var rdr = new System.IO.StreamReader(resp.GetResponseStream()))
do
{
var psres = POSHelp.Invoke(C8StreamReaderScript, rdr.ReadLine(), tbl.Columns);
foreach (PSObject pso in psres)
{
yield return pso;
}
} while (true);
}
}
IEnumerable<PSObject> resStream = C8core.OpenStream(streamurl);
resStream.ForEach(pso => WriteItemObject(pso, path, false));
Update:
one advantage the PowerShell provider has over Coral8 studio is the ability to explore a remote server's stream. With Coral8 studio if you connect to a remote server you can look at status information, but stream and window information is hidden.
Here's how to use the provider with a remote server:
>new-psdrive c8Remote Coral8 ccl://UATserver:6789
Name Provider Root
---- -------- ----
c8Remote Coral8 ccl://UATserver:6789
> get-psdrive -psprovider Coral8
Name Provider Root
---- -------- ----
C8Local Coral8 ccl://localhost:6789
c8Remote Coral8 ccl://UATserver:6789
> ls c8Remote:\Default\Project1
Name
----
outAlert
...
> ls c8Remote:\Default\Project1\outAlert
Timestamp : 8/13/2008 9:10:40 AM
EventName : Desk.Execution.Buy
Id : 1218471879101908Desk37
...
A little something I cooked up last night
# ./Get-C8Tuple ccl://localhost:6789/Stream/Default/Proj/outStream
param([string]$curl,[int]$maxRows=-1)
function Resolve-Url($url)
{
$wc = new-object Net.WebClient
$urib = [UriBuilder] $url
$urib.Scheme = "http"
$urib.Path= "/Manager/ResolveUri"
$wc.UploadString($urib.uri,$url)
}
function Get-TD($url)
{
$urib = [UriBuilder] $url
$urib.Scheme = "http"
$urib.Path= "/Manager"
$wc = new-object Net.WebClient
$wc.Headers.Add("Content-Type",'application/soap+xml; charset=utf-8; action="urn:coral8.com:SOAPAction"')
$soapBody = "<soap:Envelope xmlns:soap='http://www.w3.org/2003/05/soap-envelope' xmlns:soapenc='http://www.w3.org/2003/05/soap-encoding' xmlns:tns='urn:coral8.com:Manager' xmlns:types='urn:coral8.com:Manager/encodedTypes' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'><soap:Body> <tns:GetTD><uri xsi:type='xsd:string'>$url</uri></tns:GetTD></soap:Body></soap:Envelope>"
$soapResp = $wc.UploadString($urib.uri,$soapBody)
$td = ([xml]$soapResp).Envelope.Body.GetTDResponse.Return."#text"
([xml]$td).TupleDescriptor.Field
}
function ConvertFrom-C8CSV($csv,$names,$types)
{
# TODO - support quoted csv values
$vals = $csv.Split(",")
$o = new-object PSObject
for ($ii=0;$ii -lt $vals.Count;$ii++)
{
$t = $types[$ii]
if ($t -eq "datetime")
{
$v = ([datetime]"1/1/1970 GMT").AddTicks( ([long]$vals[$ii]) * 10 )
}
else
{
$v = $vals[$ii] -as $t
}
$o | add-member -membertype noteproperty -name $names[$ii] -value $v
}
return $o
}
function ConvertFrom-C8TypeName([string] $c8Type)
{
switch -wildcard ($c8Type)
{
"Integer*" { return "int" }
"Long*" { return "long" }
"Float*" {return "double" }
"Bool*" { return "bool" }
"String*" { return "string" }
"Time*" { return "datetime" }
"XML*" { return "xml" }
default { throw "Type '$c8Type' not implemented" }
}
}
write-progress "Resolve URL" "Pending"
$url = Resolve-Url $curl
$td = Get-TD $curl
$fieldNames = @("Timestamp") + ($td | % { $_.Name })
$fieldTypes = @("datetime") + ($td | % { ConvertFrom-C8TypeName $_.type })
write-progress "Resolve URL" "Done"
$urib = [UriBuilder] $url
$urib.Scheme = "http"
$req = [Net.WebRequest]::Create($urib.uri)
$req.Headers.Add('X-C8-StreamFormat', 'CSV')
$req.Headers.Add('X-C8-StreamFormatOptions', 'TitleRow=false')
$req.Method = "GET"
$resp = $req.GetResponse()
$rdr = [IO.StreamReader] $resp.GetResponseStream()
$cnt=0
while ($MaxRows -eq -1 -or $cnt -lt $MaxRows)
{
write-progress "Reading tuples" "in progress" -currentOperation "count - $cnt"
$csv = $rdr.ReadLine()
ConvertFrom-C8CSV $csv $fieldNames $fieldTypes
$cnt++
}
$resp.Close()
Code comments -
- I couldn't find a WSDL for the manager service or a light weight .net/powershell SOAP api, so I had to resort to sending in a canned request.
- A nice Powershell feature is how the casting operator will search the type's constructors to new up the object needed. I love it.
So my coworker, err boss :) seems to have started playing with Powershell, which is great. It looks like his first foray into PowerShell development is a commandlet for tibco ems topics. My initial feeling on this is that the benefits of commandlets syntax checking are not worth the tax of compiling and installing it. I would've used a native PowerShell approach. On the other hand, a full navigation provider...
function Get-TibcoTopic([string]$topicName,
[string]$url="tcp://localhost:7222",
[string]$username="admin",
[string]$password)
{
$null = [Reflection.Assembly]::LoadFrom("C:\tibco\ems\clients\cs\TIBCO.EMS.dll")
$null = [Reflection.Assembly]::LoadFrom("C:\tibco\ems\clients\cs\TIBCO.EMS.ADMIN.dll")
$admin = new-object TIBCO.EMS.ADMIN.Admin -arg $url, $username, $password
$admin.Topics | ? { $_.Name -eq $topicName }
}
But I am seeing this a lot with myself and coworkers around new technologies. I guess the first step in learning a technology is getting something that works, and the second phase is getting something implemented the right way. For me, I'm struggling with WPF. I wanted to implement a bullet graph
and do with the right way so that if I wanted a more traditional gauge approach, highway just need to swap out the template. I couldn't get it to work so I switched to implementing the control as the panel. It feels wrong, and I know I'm going to run into trouble with it shortly, but at least it works.
It's no secret among my friends and family that I have RSI (Repetitive Strain Injury) due to computer use. I haven't blogged about it because, well, it's personal, and because I didn't want to jeopardize any chance of future employment. But I periodically get asked for advice on how to deal with RSI pain and for whatever reason I'm feeling more confident about sharing this.
Doctors
You should see a doctor. Don't expect a cure, but it's important to rule out any more serious diseases that your RSI might be a symptom of. Additionally, RSI specialists can guide you on your options for treatment, and run interference between you, your employer, insurance companies, and those requiring prescriptions.
In New York there are a number of RSI specialists:
Ergonomics
A consultation from an occupational therapist can help a great deal. Where as a doctor will ask you to describe what you do, an occupational therapist will observe what you actually do. The difference might be quite important in finding treatment. Additionally occupational therapists will have more specific advice on ergonomic equipment purchases.
Equipment
Keyboards
There are a lot of keyboards on the market, expect it to take some time to find the right one for you, but again don't expect a miracle cure.
Pointing devices
Mice are a disaster, ergonomically speaking, lots of people like trackballs as a replacement. I loves me my Wacom Graphire tablet.
Speech Recognition
Speech recognition is one of the most powerful equipment options, but with great power comes a big learning curve. The technology is effective enough to allow you to dictate most documents, emails, and blog posts. But don't expect Star Trek like accuracy.
Even though Windows Speech Recognition has come a long way, for those with typing injuries it's still not good enough and Dragon Naturallyspeaking is really the only option worth considering. But make sure you get a good microphone.
Treatment
Physical Therapy
Physical therapy is frequently prescribed. As with nearly everything related to RSI YMMV, so make sure your therapist is familiar with RSI injuries. In NY, Joint Effort Physical Therapy seem to have a clue.
Acupuncture
American Style Acupuncture Physical Medicine, developed by Dr. Mark Seem can be very effective in alleviating pain
Surgery, Drugs, Chiropractors
Be skeptical. For surgery recommendations in particular, get second and third opinions.
I've added a few more features to the Fuzzy Task List so that it's now approaching the point where it's usable. The code's been cleanup up and released to CodePlex.
Fuzzy task lists are useful for kinds of tasks that don't have fixed times, such as:
- getting a hair cut
- scheduling your dentist appointment
- keeping in touch with old friends
Unlike the actual appointment for the dentist - 4:45pm on Tuesday, the reminder to schedule the appointment doesn't really work for these kind of activities. There's no impact if you miss the date by a few days and often there are more important things to do. The fuzzy task list solves that problem.
Fuzzy Task List CodePlex project site
The project site has the code in source control and .rar package. You can also install a ClickOnce preview.
If you'd like to contribute, just drop me a note.
A coworker asked me if I knew of any good books on .Net multithreaded programming.
The only one I knew of was .NET Multithreading which is weak in some areas and getting a little bit long in the tooth.
One of the best sources of info is Joe Duffy's blog.
Start with Concurrency and the impact on reusable libraries, keep an eye out for his book, Concurrent Programming on Windows, and consider a Safari rough cuts subscription.
More Posts
Next page »