tag:blogger.com,1999:blog-26917996281677538412024-02-18T21:58:27.745-05:00Digging in the dirtThings I do with SmalltalkBob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.comBlogger30125tag:blogger.com,1999:blog-2691799628167753841.post-27268058903935190482016-09-12T10:56:00.001-04:002016-09-12T10:57:43.105-04:00A year goes by... <span style="font-family: inherit;">September 14, 2015, we launched a new ERP system, written in Smalltalk, running on GemStone and developed with VisualWorks. The system continues to grow, we keep adding features, and our users are mostly happy. </span><br />
<br />
It got me thinking about my relationship with the company and the project.<br />
<br />
<span style="font-family: inherit;">Projects have tension between the technical and business needs. The person paying the bills makes the final call and they are being asked to take a leap of faith; they don't see what the developers see. It takes time to build up trust, yet most of the key decisions, like which technology to use, are made at the start of the project, long before the technical team is truly trusted. </span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">In our case we got a lot right: use Smalltalk to deal with unique and complex business needs, use GemStone as the database to avoid the cost of object to relational mapping, use a web interface to avoid fat client issues, and use Seaside to allow for a single technology stack (we're Smalltalk all the way down). </span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">We got a few things wrong. The worst was thinking that an old fat client framework was worth keeping. It wasn't, and I strongly argued against it. But that's a tough call for someone that is not familiar with the code. They see a sunk cost. How could it <i>not </i>have value? </span><br />
<span style="font-family: inherit;"><br /></span>
<span style="font-family: inherit;">Over time everyone realized just how bad the old framework was, but by then we had invested a lot of time and effort making the domain code run in a new web framework. We're still struggling to remove the last bad bits. But I can see the risk management decision on this: it was scary to agree to throw away the old code and move to something new and unproven. </span><span style="font-family: inherit;">It's self evident now; it wasn't then. </span><br />
<br />
But it made me think: just how relevant is the technology decision, like which language or framework to use? Our users don't care. They need tools to do their job. Management doesn't care. They want IT to provide services at minimal cost. As a Smalltalk team we're very efficient. But so what? A java team would be easier to staff. Development would take longer, but they could get temporary help up to speed quickly to help get over humps. Technical consultants would actually be helpful (virtually none of the ones we've worked with knew Smalltalk).<br />
<br />
And it's a general problem to anyone advocating an unconventional technology. Business might invest in a Smalltalk project if they see a return on investment, and if the risk is acceptable. But selling that vision in a world of deafening silence about Smalltalk is tough.<br />
<br />
I haven't lost faith. Using Smalltalk allows us to be flexible in ways other teams could only dream of. Things will get a lot better, once we've scraped off the last of the old framework and are able to focus all of our time on building new stuff. I see a future where the development team is seen as a partner is the business. Where our ability to see business patterns and user flows gives us a voice. Where we're not just a cost of doing business.<br />
<br />
That's my vision: that Smalltalk projects allow the developers to be partners in the business, since they don't need to wallow in technical minutiae. They can stay in the business head space, so they can add value beyond the code. I see that happening in our project, and I think it's an important part of the story when advocating for Smalltalk.<br />
<br />
I am looking forward to the next year.<br />
<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com5tag:blogger.com,1999:blog-2691799628167753841.post-47274037331436417052016-01-03T10:05:00.001-05:002016-01-03T10:05:52.427-05:00Lessons learnedThe project I've been working on since May, 2012, went live September 14, 2015. It's an ERP system for a sales company, which specializes in industrial HVAC rep sales (where you 'represent' the manufacturer). It is nice to announce the deployment of a 100% Smalltalk application, built with VisualWorks, GemStone and Seaside. <br />
<br />
Our users are happy, mostly. They want more features, and they want them sooner than later. Not a bad place to be. <br />
<br />
Personally, it's been both a rewarding & frustrating project. Rewarding because I get to work for a far-sighted company that sees the value of a custom application, and can deal with the risks of using a niche technology. Frustrating because it could have been done better (which, I suspect, is true of just about any project).<br />
<br />
The past couple of years have been a head down, ignore everything else, focused effort. We've done some interesting things, many of which I had hoped to share, but there never seems to be any time; work takes it all.<br />
<br />
So, with the benefit of hindsight, here is what I've learned...<br />
<br />
<b>Have a</b><b> project champion</b><br />
Our project champion is one of the founders of the company. Without him risks would not have been taken, and the project would not have happened. We replaced a 20+ year old custom system that was also championed by the same person. He, and the company, believe that a custom ERP provides a competitive advantage. The old software proved it, which made selling the idea of a new system easier.<br />
<b><br /></b>
<b>Smalltalk productivity rocks</b><br />
Total effort was about 16 to 18 person years (our team size varied from 3 to 5 over 3.5 years). Compare that with effort to deploy something like SAP, and we look good. Our team's productivity will really shine as new features and customizations are rolled out over the next couple of years.<br />
<br />
<b>Expect a long tail of trivial things</b><br />
What really stands out is how much time we spent (and continue to spend) on the little things. It tends to be boring, almost clerical work. But it's what users notice. Font sizes, colours, navigation sequences, default values, business rule adjustment... nothing intellectually challenging.<br />
<br />
The beginning of the project was fun: figuring out how to use thousands of VW window specs in a Seaside application, including modal dialogs and dynamically morphing views. Finding ways to hold complex updates prior to a Save / Cancel decision. Building a new report framework that allows for edits and generates PDF content. Adding application permissions. Implementing a RESTful web-to-GS mechanism. And so on. All good stuff, but mostly done.<br />
<b><br /></b>
<b>Pay your technical debt early</b><br />
Looking back, we would have been better off not trying to preserve the ecosystem of the old fat client framework (the idea was to keep most of the domain code as is). Instead, we should have started from scratch, using the old system as a spec. The old framework was garbage. We knew it, but thought the technical debt could be managed. It was, but at a cost. We now know where we spent our time; it's evident switching to new code earlier would have allowed us to be deployed earlier.<br />
<br />
If you see garbage code, be ruthless and get rid of it. Bad code is like a bed bug: it will keep biting until properly exterminated.<br />
<b><br /></b>
<b>Show progress</b><br />
Users need to see progress. And developers need feedback. We hit the jackpot with our beta users: they were willing to put up with a lot of early unfinished code. It gave them a view of what was to come, and they communicated that to the rest of the company. Our project dragged on a bit, but they saw progress, which made the delays palatable. <br />
<br />
<b>Have clear metrics</b><br />
It's so easy to get caught up in the moment, and to work on what is of interest right now, because that is what users see. But if you do that, you'll forget about the long term, and the important internal stuff just won't get done. If developers are not measured on the long term deliverables, there is little incentive to work on them.<br />
<br />
Make long term metrics just as visible as short term ones. Break them down and make them part of each iteration, even if they are obscure and of no interest to the end users. It will be frustrating, You'll get asked "why are you working on that and not the feature I'm waiting for?". But they'll be far more aggravated if the application is not reliable. It's like backups: you don't notice their absence until you need them. Be sure they see the value of the boring internal tech stuff.<br />
<br />
<b>Use agile development </b><br />
We release a new production version every two weeks, with minor changes published twice each week. Developers merge their code every couple of hours. All new code is expected to have an SUnit test. The full test suite is run each night with Jenkins and keeping test green is the first developer priority. We pair up for tricky problems. Refactoring is considered to be a 'technical investment'. All changes are tracked (we have a nifty issue management tool).<br />
<br />
Reviewing the process is part of the process. We adjust things almost every week. It's not easy, but getting to a smooth productive rhythm is so worth it.<br />
<br />
What next? Mobile web interfaces, an Android app (we can do that with Pharo once a VM is ready), moving to a GS Seaside interface (need to port PDF4Smalltalk to GS), and a lots of small stuff. <br />
<br />
<br />
<i style="background-color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18.2px;"><span style="font-size: xx-small;">Simple things should be simple. Complex things should be possible. - Alan Key</span></i><br />
<br />Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com3tag:blogger.com,1999:blog-2691799628167753841.post-22480017641707260622014-11-13T16:38:00.001-05:002014-11-13T16:46:36.739-05:00GemStone based reports & viewsMy current project is a port of a VisualWorks & GemStone fat client application to Seaside. Part of the porting effort was to map a few thousand VW windowSpec views to a Seaside web views. It all works fine, but it's not ideal. Ported fixed layout views look like fat client windows; they lack a web aesthetic. We want all new views to be more 'web-centric', where positioning and sizing is adjusted by the browser, especially from tablets and mobile devices.<br />
<br />
We also need to provide reports. For a web app, answering a PDF for a report works well.<br />
<br />
We combined these two requirements and ended up with reports generated on GS using a Seaside-like coding pattern, which is then rendered in by Seaside in VW, and can be viewed as a PDF.<br />
<br />
To build the reports we use <a href="http://www.stic.st/wp-content/conferences/2012/Monday/1330-pdf4smalltalk_based_Report_Framework-Nemec.pdf">Report4PDF</a>, something I wrote a few years ago. It uses <a href="https://gitorious.org/pdf4smalltalk/pages/Home">PDF4Smalltalk </a>to generate a PDF document. PDF4Smalltalk has not been ported to GemStone, something I'd like to do when time allows (and to VA & Pharo). Fortunately, Report4PDF generates intermediate output before requiring PDF4Smalltalk. This output <i>can </i>be created on GS, which is then moved to VW, where PDF4Smalltalk is used for the final output.<br />
<br />
Our VW to GS interface uses only strings, either XML or evaluated command strings. In this case, the report objects are packaged as XML, and then recreated on VW. For most reports building and parsing the content takes about 200ms (we may move this to a command string, which is typically a third faster).<br />
<br />
Once the report is in VW we use a 'report component' for the rendering, which reads the report content and builds the Seaside output. Because Report4PDF has a Seaside-like coding style, the mapping is relatively simple.<br />
<br />
For example, a table is defined as...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">aTable row: [:row | </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell: [:cell | cell widthPercent: 20. cell text bold; string: 'Job'].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell: [:cell | cell widthPercent: 30. cell text; string: self job description].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell: [:cell | cell widthPercent: 20. cell text bold; string: 'Our Job ID'].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell: [:cell | cell widthPercent: 30. cell text; string: self job id]].</span><br />
<br />
...and gets rendered as...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEBT1jokz-RUut2a_0HCPR2CRK4NvNE3b_necCjLXlETrP20sE7WYsWxplyrHPRS8Qu8GelL6gN0gYUpwIxIeAi2mxoqtJJvb2Kt4-0zoNzzN44xlKgqyqSZZjGxzGarVZdFNVDHmPLXJ3/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEBT1jokz-RUut2a_0HCPR2CRK4NvNE3b_necCjLXlETrP20sE7WYsWxplyrHPRS8Qu8GelL6gN0gYUpwIxIeAi2mxoqtJJvb2Kt4-0zoNzzN44xlKgqyqSZZjGxzGarVZdFNVDHmPLXJ3/s1600/1.png" /></a></div>
<br />
<br />
...the PDF output is...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsZ3qkictMU88RYa3YXgzKQKJAI8rMkPc3_WK4abg58mAY6qBC22WyndG3x22TGURqlS7bU6p_rqaoY7ieb0anTXRycZ6OJvWdrthdndFjaVfXW2bbfKFniK2DB0hwMRMGcA1TBGeydUlo/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhsZ3qkictMU88RYa3YXgzKQKJAI8rMkPc3_WK4abg58mAY6qBC22WyndG3x22TGURqlS7bU6p_rqaoY7ieb0anTXRycZ6OJvWdrthdndFjaVfXW2bbfKFniK2DB0hwMRMGcA1TBGeydUlo/s1600/2.png" /></a></div>
<br />
<br />
...to build the PDF content we use the data already in VW. No additional GS call is needed.<br />
<br />
R4PObject, the root Report4PDF class, has a #properties instance variable to support extensions. We use this to add link and update capabilities to the report when it is rendered in Seaside.<br />
<br />
For example, a link to another domain object is coded as...<br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell right bold string: 'Designer'.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>row cell text normal string <b>linkOop: self designer domainOop</b>; string: self designer displayKeyString.</span><br />
<br />
...and displayed as...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLziOoIhiOg29Q6oJ_UiUaKnK8wxs-r3Qkdk-KyXmsDfIITKnXpTdOHY331giAF_y2_nFWS1mnoYpoewfqNJ7wxd6rFXXJpyztEpM4U5mLwxUIlruhM5vCZEA2KH2oDCrLAS8y2JnfUqMl/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLziOoIhiOg29Q6oJ_UiUaKnK8wxs-r3Qkdk-KyXmsDfIITKnXpTdOHY331giAF_y2_nFWS1mnoYpoewfqNJ7wxd6rFXXJpyztEpM4U5mLwxUIlruhM5vCZEA2KH2oDCrLAS8y2JnfUqMl/s1600/3.png" /></a></div>
<br />
<br />
...but is ignored in the PDF output...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLYfyl6Uzdm78Q5xcMXaj0d5mP2CYBa6RlPOR1xe2oOquMbUeHxAzJkj3ORUxF0UrblMBHkwgDGOEivZKBXulF2iyWYbSQ9htezYcluGgyX_Ty-i7qppnchxiQBecJIe6sH9hxibudVTpR/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLYfyl6Uzdm78Q5xcMXaj0d5mP2CYBa6RlPOR1xe2oOquMbUeHxAzJkj3ORUxF0UrblMBHkwgDGOEivZKBXulF2iyWYbSQ9htezYcluGgyX_Ty-i7qppnchxiQBecJIe6sH9hxibudVTpR/s1600/1.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
The beauty of this approach is that all of the report generation is done on GemStone, with generic rendering and PDF generation in our VW Seaside code.<br />
<br />
Our users are happy with this approach. They like the look of the web rendered report and the option to get the content as a PDF. Having link and simple update capabilities means that most users will not need to use the old fat clients views, which tend to be used by power users, for data entry and for detailed updates.<br />
<br />
<i><span style="font-size: x-small;">Simple things should be simple. Complex things should be possible. - Alan Key</span></i>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-73429616411100082892014-05-22T11:33:00.000-04:002014-05-22T11:33:18.581-04:00Smalltalk performance measurement The application I'm working on uses VisualWorks and GemStone. As we've built out our application and loaded more test data we find ourselves spending more time turning performance. If there is one thing I've learned over the years that is performance problems are never what they seem: always measure before you change the code. Premature optimization makes your code ugly and, more likely than not, adds no value.<br />
<br />
On VW we use TimeProfiler & KaiProfiler, and on GS we use ProfMonitor. All are useful for getting a sense of where to look, after which we switch to more basic tools, like...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Time>>millisecondsToRun:, along with some convenience methods.</span><br />
<br />
In VW you can use the Transcript to show performance measurements.<br />
You could write something like...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Transcript cr; show: 'tag for this code'; show: (Time millisecondsToRun: [some code]) printString.</span><br />
<br />
...but that's a pain. And you'd need the 'tag for this code' if you have several measurements spread throughout the code. To make that easier, we use...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">'tag for this code' echoTime: [some code]</span><br />
<br />
...which is implemented as...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">echoTime: aBlock</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| result microseconds | </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>microseconds := Time microsecondsToRun: [result := aBlock value].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self echo: microseconds displayMicroseconds.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^result</span><br />
<br />
...the #echo: method is commented on <a href="http://smalltalk-bob.blogspot.ca/2011/04/smalltalk-trivia.html">in a previous post</a> and #displayMicroseconds is just...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Integer>>displayMicroseconds</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self > 1000 ifTrue: [^(self // 1000) displayTime].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^self printString, 'µs'</span><br />
<br />
...and displayTime shows hh:mm:ss.mmm with hh and mm displayed if needed.<br />
<br />
In GS you could use the VW transcript with a client forwarder, but our application uses a simplified GS interface model with only one forwarder and no replication (an XML string is the only returned value from GS), so adding a client forwarder was something I did not want to do. I also wanted to run some tests from a Topaz script.<br />
<br />
Instead of a Transcript I use a String WriteStream held in a GS session variable and use Time class methods to add measurements and show the results. To measure a block of code, we add nested time measurements with...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Time log: 'tag for this code' run: [some code]</span><br />
<br />
...and we wrap the top method send with...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Time showLog: [top method] </span><br />
<br />
...some methods get called a lot, so we'd like a total time. For that we use...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">Time sum: 'tag for this method' run: [some code] </span><br />
<br />
...because each Time method answers the block result we can insert the code easily...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">someValue := self bigMethod</span><br />
<span style="font-family: Courier New, Courier, monospace;">...vs...</span><br />
<span style="font-family: Courier New, Courier, monospace;">someValue := Time log: 'bigMethod' run: [self bigMethod]</span><br />
<br />
<br />
These are the methods...<br />
<br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">Time>>showLog: aBlock</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeSumDictionary: Dictionary new.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeLogStream: String new writeStream.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeLogStream nextPutAll: 'Time...'.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self log: 'time' run: aBlock.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^self timeLogStream contents , self displayTimeSums</span><br />
<br />
...each time* variable is stored in the GS session array, like...<br />
<br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">timeLogStream</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^System __sessionStateAt: 77</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">timeLogStream: anObject</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>System __sessionStateAt: 77 put: anObject</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">log: aMessage run: aBlock </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>"Time showLog: [Time log: 'test' run: [(Delay forSeconds: 1) wait] ]"</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| result microseconds | </span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>microseconds := self millisecondsToRun: [result := aBlock value].</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeLogStreamAt: aMessage put: microseconds.</span><br />
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^result</span><br />
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span></div>
<div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">timeLogStreamAt: aMessage put: anInteger</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| stream | </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>stream := self timeLogStream.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>stream isNil ifTrue: [</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>stream := String new writeStream.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeLogStream: stream].</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>stream </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>cr; nextPutAll: aMessage; tab; </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>nextPutAll: anInteger displayTime.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeSumDictionaryAt: aMessage add: anInteger.</span></div>
</div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span></div>
<div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">sum: aMessage run: aBlock </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>"Time showLog: [</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>Time sum: 'test' run: [(Delay forSeconds: 1) wait].</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>Time sum: 'test' run: [(Delay forSeconds: 1) wait]]"</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| result milliseconds | </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>milliseconds := self millisecondsToRun: [result := aBlock value].</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeSumDictionaryAt: aMessage add: milliseconds.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^result</span></div>
</div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><br /></span></div>
<div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;">timeSumDictionaryAt: aKey add: aValue</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| dictionary total | </span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>dictionary := self timeSumDictionary.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>dictionary isNil ifTrue: [</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>dictionary := Dictionary new.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self timeSumDictionary: dictionary].</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>total := dictionary at: aKey ifAbsent: [0].</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>total := total + aValue.</span></div>
<div>
<span style="font-family: Courier New, Courier, monospace; font-size: x-small;"><span class="Apple-tab-span" style="white-space: pre;"> </span>dictionary at: aKey put: total.</span></div>
</div>
<div>
<br /></div>
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-64953036250201412752013-11-09T11:22:00.000-05:002013-11-09T11:22:51.127-05:00Taxonomy of IgnoranceI've spent most of this year wallowing through a huge amount of old bad code. Diving into this mess got me thinking about Prof. Samuel Holtzman's "Intelligent Design Systems", a book I read about twenty years ago (<a href="http://www.youtube.com/watch?v=z-gTy8cVofg">here's his TED talk</a>). It changed how I approach the unknown: rather than seeing it as a big blob of nothing, I apply Holtzman s "Taxonomy of Ignorance" and dive in. The idea is to look at what you don't know and categorize it; understanding your ignorance makes it manageable.<br />
<br />
Here's my programmer-biased view of Holtzman's taxonomy...<br />
<br />
<b>Combinatorial </b>('Computational' in the TED video)<br />
You know there is a solution and you know how to get it, but the calculation costs prevent you from getting the answer; the foundation of encryption.<br />
<br /><b>Watsonian</b><br />
You have a complete model of the problem but lack an effective solution method, like Dr. Watson and Sherlock Holmes. Or the uber programmer that can see a solution that no one else sees, even though everyone has the same data.<br />
<br /><b>Gordian </b>(interesting that in the TED talk this comes after Ptolemaic; this is order in the book)<br />
What do you do when you no longer have a complete model of the problem? Whatever solution method you have will not work; you need to restate the problem. Alexander the Great could not untie the knot that Gordius, king of Phrygia, tied for the future ruler of Asia. Instead, he reframed the problem of untying it and simply cut the knot.<br />
<br />
Our team is dealing with code we find incomprehensible. It was written over a dozen years with little regard for maintainability and is the worse example of technical debt I've ever seen. Instead of trying to unravel it, we've decided to bypass it: figure out the API methods, isolate parts, replace them, and discard the old code. The job changed from 'make the old code work' to 'make the application work'; cutting was more effective than fixing.<br />
<br /><b>Ptolemaic</b><br />
In this case you have a solution to the problem that is based on an inadequate model, like the Ptolemaic earth-centric model of the solar system, vs. the more elegant Copernican model. Or, to put it another way, if all you have is hammer, everything looks like a nail.<br />
<br />
Computer programming is in that state: schools teach Java almost exclusively and students see "programming" through a procedural C syntax filter. It's sad that they are not exposed to Prolog, Lisp, Smalltalk, assembler, SNOBOL ... anything that would shake up their mental model, and maybe even provide a more elegant one.<br />
<br /><b>Magical</b><br />
You know something works, but it contains essential elements for which you have no effective formalization. Your car works, you rely on it, but when it breaks you suddenly realize that you do not know how it works. And that's a fair deal: we can't know everything. We do rely on the knowledge of others.<br />
<br />
I'm happy to program without knowing all the details of how my keystrokes translate into electrical impulses that eventually render images on a screen, but I sure do notice when it breaks.<br />
<br />
When it comes to code that I'm responsible for, magical ignorance is not an option. Our project schedule has slipped because of this: we'd rather delay deploying than rely on code that we don't understand.<br />
<br /><b>Dark</b><br />
When we lack a model we have not way to approach a solution. "What is life? Why are we here?" ... not a comfortable question, so some find refuge in religion and faith. Guess it's better to believe something than to admit that you just don't know.<br />
<br /><b>Fundamental</b><br />
Finally, what about cases where we don't even know the question exists? We don't have a model because we don't know to look for one. Like the innocence of youth; no three year old worries about politics.<br />
<br />
Apply this list, and add the understanding that you will know more tomorrow than you do today, you can start eating that elephant, one bit at a time.<br />
<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-71706939514720464022013-06-18T15:20:00.000-04:002013-06-18T15:20:44.277-04:00Roassal visualization of Seaside componentsAt STIC 2013 Alexandre Bergel presented <a href="http://pharobooks.gforge.inria.fr/PharoByExampleTwo-Eng/latest/Roassal.pdf">Roassal</a>, a Smalltalk visualization engine. It looked like a nice fit for our project, where we build deeply nested Seaside views from VW window specs. Navigating the component structure can be confusing, so I decided to add a tree view using Roassal.<br />
<br />
We have the ability to <a href="http://smalltalk-bob.blogspot.ca/2013/03/inspecting-nested-seaside-components.html">inspect individual components</a>, and we added our own inspector subclass which gives us a place for a custom menu (in VW you can do that by overriding #inspectorClasses). The most used menu entry is 'Inspect Parent Path', which inspects an array of components built from walking the parent links from the selected component up to the root component. <br />
<br />
The parent path is handy, but is does not provide enough context, and navigating to a component outside of the parent path is a pain. It would be better to see a parent tree, with siblings and labels. Each of our components answers #parentComponet and #components. For the parent tree we just added each parent and each parent's components (siblings) into a set. Coding it in Roassal was easy...<br />
<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">visualizeParentPath</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| view list |</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>list := self parentPathWithComponents.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view := Roassal.ROMondrianViewBuilder view: Roassal.ROView new.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view shape rectangle </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>if: [:each | each hasUpdates] borderColor: Color red;</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>if: [:each | each == self] fillColor: Color yellow;</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>withText: [:each | each displayVisualizationLabel].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view interaction </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>item: 'inspect' action: #inspect;</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>item: 'visualize' action: #visualizeParentPath.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view nodes: list.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view edgesFrom: #parentComponent. </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view treeLayout.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>view open.</span><br />
<br />
<br />
And here is what it looks like (the mouse is hovering over the 'I' input field; the popup is the printString of the component)...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWxvrERcNkci3aqeBw2L5Yro4v4Jn_dkjGxOjYLDFJIWbjhNkw-z-EbCfGav1aRSptrBQDUNoMW4pPLnAIJjvlCYVrbMlfTaviOcmSqbVw4ij6_drPqVRHEo9bYlHV9VhHmgLT0nN9wcQh/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="401" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWxvrERcNkci3aqeBw2L5Yro4v4Jn_dkjGxOjYLDFJIWbjhNkw-z-EbCfGav1aRSptrBQDUNoMW4pPLnAIJjvlCYVrbMlfTaviOcmSqbVw4ij6_drPqVRHEo9bYlHV9VhHmgLT0nN9wcQh/s640/1.png" width="640" /></a></div>
<br />
This has proven to be quite handy. A big thanks to everyone that contributed to Roassal.<br />
<br />
<span style="font-size: x-small;"><i>Simple things should be simple. Complex things should be possible.</i> Alan Kay. </span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-91176073118768496502013-05-09T09:28:00.000-04:002013-05-09T09:28:06.547-04:00Agile Ditch DiggingI'm a strong believer in agile software development. Every project I've worked on evolved into something every different from what it started as. Agile believes in trapping that type of change; it believes in understanding that you will know more as you build; agile deals with "managing ignorance".<br />
<br />
On our current project we are migrating from a VW fat client to a Seaside application with short GS transactions that uses the legacy meta model, UI layouts and data structures. Our design isolates the UI layer in the VW Seaside server and the domain layer in GS. We parse the VW window spec into Seaside components and communicate with GS using RESTful data calls (nested arrays of strings and oop numbers), and get XML back.<br />
<br />
All of that works well: it's quick, looks nice, scales and is much easier to wrap SUnit tests around. And it was built using agile development techniques. Mostly.<br />
<br />
I find that agile works best in the 'construction' side of the work, where you can define the user stories and measure the pace of delivery (the ditch digging). There is, however, another flavour of software development, the R&D or 'creative' side, like designing the framework and tools that the application code rests on. It's not something the user sees; it's just part of the application's fabric.<br />
<br />
Recently we thought about how we were dealing with widget level feedback. That's where you enter 'abc' in the 'name' field and get feedback when you move on to another field. If 'abc' is an invalid value, it would be useful to see that right then ('on blur'; when the widget loses focus), instead of waiting until the 'save' button is pressed. The same goes for updating depending values: if the 'comment' defaults to 'name', having it change when 'name' is entered is useful.<br />
<br />
Our original approach was to do this behaviour in the web code, since it had access to the display components. We soon realized that is was more important that the code have access to the domain and be able to reuse the legacy validation code, so we moved the logic to GS. Seemed simple enough.<br />
<br />
Turned out that shifting that one responsibility triggered a lot of framework redesign. Originally, the web component built up the set of changes, and passed them to GS on 'save'. It was simple and worked. With the field level code on GS, each 'on blur' event had to trigger a GS call and, more importantly, had to package the full view update state into call so the domain code would see the current displayed state . Performance is not an issue, since each call takes from 10 to 50 ms, but the code change was more complex than it originally seemed.<br />
<br />
I could find no good way to communicate the status of this 'big bang' change. The problem was complex, then things got delayed due to other work, and we made some critical design changes as we understood the technical issues better. None of that was well communicated. From the outside looking in, the project just stalled. Precisely the kind of optics you don't want, and the kind of problem that agile techniques are supposed to deal with.<br />
<br />
I simply do not know how to measure 'thinking time', especially my own. Finding a solution may take me five minutes, or five hours.<br />
<br />
It was interesting to feel the pendulum swing from 'creative' to 'construction' as the work progressed; the construction phase is so much easier. Easier to do, easier to measure and easier to manage. Everyone is more at ease when you can show that you're 80% done, vs. just telling them that "you're close".<br />
<br />
<i>Digging a ditch is easy, assuming you know how.</i><br />
<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-53835594950112065232013-03-24T17:30:00.000-04:002013-03-24T17:30:24.041-04:00Inspecting nested Seaside componentsI'm spending most of my time working on the tools to support a web port of a VW fat client application. We read windowSpecs and use them to build our own web 'widget' instances that know how to render themselves in Seaside using absolute positioning. This, combined with widget attribute meta data, has allowed us to automate most of the VW to Seaside port.<br />
<br />
A challenge with this approach is that the Seaside views can get complex, with deeply nested subcanvas components. When debugging a button or input field widget, it's nice to not have to spend time getting to the specific instance. Seaside's Halos work well for that, but not when the view has a lot of components; things get lost is the noise.<br />
<br />
A few fields...<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjo0TE1NxBUwUsiUKhl9YMnKjMenA0G58K-gYIxu9rt2j6R3jSUq26-9gVAoKHLsF9NgM8sW9MHD2RI4_laj0b8O3aVPt6dhiSyIfw2d6Ir4yNqIPyz3w_yVSH1x-gVDIHVjQV0kJuGuQVD/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjo0TE1NxBUwUsiUKhl9YMnKjMenA0G58K-gYIxu9rt2j6R3jSUq26-9gVAoKHLsF9NgM8sW9MHD2RI4_laj0b8O3aVPt6dhiSyIfw2d6Ir4yNqIPyz3w_yVSH1x-gVDIHVjQV0kJuGuQVD/s1600/1.png" /></a></div>
<br />
...is all it takes to make things messy...<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGcLgYbiMIAWyGy99JTnOpgGFrMoHz7jeiz63Y1NeTl79pXFrUSx8dx4cBp1IU14CjERF_Goc_fjgonIAd2KEljy-GyRujBmoT9hC7rvH5IvEVt1bFTumJcdzLl8GrFtaaTgNt5Q93TrQy/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGcLgYbiMIAWyGy99JTnOpgGFrMoHz7jeiz63Y1NeTl79pXFrUSx8dx4cBp1IU14CjERF_Goc_fjgonIAd2KEljy-GyRujBmoT9hC7rvH5IvEVt1bFTumJcdzLl8GrFtaaTgNt5Q93TrQy/s1600/2.png" /></a></div>
<br />
To make things easier we added a hidden 'inspect' icon to each VW based component that is only rendered if 'WAAdmin developmentToolsEnabled' is true. It's toggled by a WAToolPlugin subclass icon. Here is how the same view looks with the inspect anchors visible (the title for each inspect icon is the display string of the objedt that would be inspected when clicked)...<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Hf7IFuYVEpc8QDbJZwafZPcyaRLA3zF0F_oraYOZE-kGBAuttHsLCFAPIG0SrV_mvSdusUCdy3CAZIkbqvDO7l_7EM2_7s4zSk2ehOBWLB6AJjfZKVOxZOudExmGMl_IIGKwdErX-3ud/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Hf7IFuYVEpc8QDbJZwafZPcyaRLA3zF0F_oraYOZE-kGBAuttHsLCFAPIG0SrV_mvSdusUCdy3CAZIkbqvDO7l_7EM2_7s4zSk2ehOBWLB6AJjfZKVOxZOudExmGMl_IIGKwdErX-3ud/s1600/3.png" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
Each 'widget' is contained in a div for absolute positioning. Inside this div we added the inspect render method...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">renderInspectOn: html</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>Seaside.WAAdmin developmentToolsEnabled ifFalse: [^self].</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>html anchor</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>class: 'subcanvasInspector'; </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>style: 'display: none; position: absolute; '; </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>title: self displayString; </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>onClick: (html jQuery ajax </span><br />
<span class="Apple-tab-span" style="font-family: 'Courier New', Courier, monospace; white-space: pre;"> </span> <span style="font-family: Courier New, Courier, monospace;">script: [:s | s << (html jQuery ajax callback: [self inspect])]);</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>with: [html image style: 'width: 12px; height: 12px; '; url: RepWebFileLibrary / #inspect16Gif].</span><br />
<br />
...and then we toggle the display with...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">renderInspectWidgetsToggleOn: html</span><br />
<span style="font-family: Courier New, Courier, monospace;"> html anchor </span><br />
<span style="font-family: Courier New, Courier, monospace;"> onClick: (html jQuery class: 'subcanvasInspector') toggle;</span><br />
<span style="font-family: Courier New, Courier, monospace;"> onClick: (html jQuery class: 'subcanvasInspectorPlus') toggle;</span><br />
<span style="font-family: Courier New, Courier, monospace;"> with: [</span><br />
<span style="font-family: Courier New, Courier, monospace;"> html image </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> class: 'subcanvasInspectorPlus'; </span><br />
<span class="Apple-tab-span" style="font-family: 'Courier New', Courier, monospace; white-space: pre;"> </span> <span style="font-family: 'Courier New', Courier, monospace;">url: Portal.RepWebFileLibrary / #inspectPlus24Gif.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>html image </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> class: 'subcanvasInspectorPlus'; </span><br />
<span class="Apple-tab-span" style="font-family: 'Courier New', Courier, monospace; white-space: pre;"> </span> <span style="font-family: 'Courier New', Courier, monospace;">style: 'display: none; '; </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> url: Portal.RepWebFileLibrary / #inspectMinus24Gif].</span><br />
<br />
<br />
Off...<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirwnPc__mjp5OcADIGRrVOzF0vs9BiArljXkHgFa_0fXPP-e7xtZSDxe2qnNdNO1Vjh5fpOv5bakFp4Uy77iouoUcaMNz4j-p2ipCc_Su_Wa-6TomlL0acAidRA3zBxJ4ciuJMKBzU8YSW/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirwnPc__mjp5OcADIGRrVOzF0vs9BiArljXkHgFa_0fXPP-e7xtZSDxe2qnNdNO1Vjh5fpOv5bakFp4Uy77iouoUcaMNz4j-p2ipCc_Su_Wa-6TomlL0acAidRA3zBxJ4ciuJMKBzU8YSW/s1600/4.png" /></a></div>
On...<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZNlJJWpg5FhOQGSftXebTEtY9MdltqrMnPbdFVxNlnXRzPTyrA9PZT8km17lTPSbd1zcalcZVvSNQx6VK2wdsjjU8NUJdjoDAOoRdn5VlcGgEqvdB6JslSSAjSmIsFLwvY-K2EyAkxGqx/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZNlJJWpg5FhOQGSftXebTEtY9MdltqrMnPbdFVxNlnXRzPTyrA9PZT8km17lTPSbd1zcalcZVvSNQx6VK2wdsjjU8NUJdjoDAOoRdn5VlcGgEqvdB6JslSSAjSmIsFLwvY-K2EyAkxGqx/s1600/5.png" /></a></div>
<br />
The other icons are for root component inspect, matching VW component inspect and a button to open the VW view (the deployed Seaside image has no VW domain view classes; these are being used during the application port).<br />
<br />
With this setup we're able to port a VW application with 4497 window spec methods and keep our manual code work manageable.<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com2tag:blogger.com,1999:blog-2691799628167753841.post-70854410116363571002013-02-24T12:54:00.003-05:002013-02-24T12:55:39.296-05:00RESTful GemStone with multiple sessionsThe project I'm working on is moving to a full web deployment from a classic VW fat client + GS. Our layers are now Seaside on VW and GemStone. To allow for this migration, we've done interesting work with VW window specs and domain meta data, which I'll comment on in future posts. Here I'll go over how we are interfacing to GemStone from our Seaside server.<br />
<br />
Our goal is to have a snappy application, so we view Seaside as a simple presentation layer. It contains no domain classes, only view components with widgets that are coded to know which attribute they display. Getting data from GS is done by packaging the list of displayed attributes, along with the oop of the displayed object, and sending it to GS. GS answers an XML string, which is parsed into 'domain node' objects (generic data holders) and then used by the display.<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">xmlDomainNodeOop:attributes:collectionOop: #(6944141057 #('comment') 656304641)</span><br />
<br />
<br />
<span style="font-family: Courier New, Courier, monospace;"><domain></span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>6944141057</oop></span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><objectClassName>BIDcustomer</objectClassName></span><br />
<span style="font-family: Courier New, Courier, monospace;">...</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><domain></span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><objectClassName>ByteString</objectClassName></span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><text>comment</text></span><br />
<span style="font-family: Courier New, Courier, monospace;">...<span class="Apple-tab-span" style="white-space: pre;"> </span><value>a comment string</value></span><br />
<span style="font-family: Courier New, Courier, monospace;"></domain></span><br />
<br />
We develop in a full VW client, so debugging is done by sending the #xmlDomainNodeOop:attributes:collectionOop: method in a workspace. Very handy when trying to recreate a user problem.<br />
<br />
If you've ever worked with GemStone (and, if you're programming in Smalltalk, my sympathies if you have not), you would be familiar with the GBS interface, the magical code which keeps objects in sync between the client and the server. You can choose to have methods execute where they make most sense (like UI heavy methods on the client and big data footprint methods on the sever) and know that your objects are in the correct state in both environments. Very cool.<br />
<br />
With our oop + attributes question and XML answer, we don't make use of that GBS feature. In fact, we'd prefer a way to turn it off. Since each request and answer pair is independent, there is no need for session state and we can run each Seaside server with multiple sessions, using round robin dispatching.<br />
<br />
Doing that introduces a few technical curiosities (and a huge thanks to Martin McClure for his help in fixing them). First, you can't share a class connector between GS sessions. In the single session model, I had a thin 'client dispatcher' class that forwarded class messages to server class that interfaced with the domain model and provided tools for building the XML answer. For multiple sessions, I had to link to an instance, so both class became singletons and each session defined its own connector.<br />
<br />
Next, we tripped over the fact that you can't share objects between sessions. That makes sense in a GBS model, since syncing the objects is a session responsibly. But we were not passing domain objects and each request and answer were done with new Array and String instances. Turns out the problem was a method with an attribute parameter coded as <span style="font-family: Courier New, Courier, monospace;">#( ('value') )</span>. Our parameter copy was not deep enough, so each tried to connect the nested array and we ended up with the error...<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace; font-size: 9.0pt; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-font-family: Calibri; mso-fareast-language: EN-US; mso-fareast-theme-font: minor-latin;">Attempt to associate
a Array with more than one GemStone session</span></blockquote>
Replacing the parameter with 'Array with: (Array with: 'value')) fixed the problem. We've since added our own deepCopy extensions to prevent these types of problems in the future.<br />
<br />
The session round robin mechanism is done with our on GS session wrapper. A class collection of session instances is rotated through, with the 'next session' pointer incremented after each use. If a session semaphore is busy, we skip to the next one. If they're all busy, we wait on the one we started with. We've been testing with three sessions, and since most of our GS session access takes less than 50ms, it's very rare that we a delay due to a busy session. We still have the occasional outlier (like the five seconds in this sample), but most of those are due to GS faulting pages in our test environment. We expect our production server to have everything cached.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwQ-kBsDANaiqak1KCgjZ9m79r_tGp8P6pb1dLUT-RQaEonjUZcoDhGm_ETf_kBZ5i8eTVRjRxq-Iqa4chjcq7AjXYMt8lTam3DbpaZL64CaAJhQqX5AJd7ISCPefAsL5gnBONIeMXVpUM/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="213" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwQ-kBsDANaiqak1KCgjZ9m79r_tGp8P6pb1dLUT-RQaEonjUZcoDhGm_ETf_kBZ5i8eTVRjRxq-Iqa4chjcq7AjXYMt8lTam3DbpaZL64CaAJhQqX5AJd7ISCPefAsL5gnBONIeMXVpUM/s400/1.png" width="400" /></a></div>
<br />
<br />
It will be interesting to see how well we scale. Our system uses a dispatcher image to direct Seaside sessions to GS images, so we can adjust to load by increasing the number of Seaside images, and the number of GS sessions each image can support.<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ0SvS2poy_uhHz_FrZ_rlbHehlC7NkyUFDxCNnLV6JCTCPkOWQFg05sCw9aeUSl8rBKhC5ewzh7Rsdt8cw-hxyqmAGdkYsJa6WSw-bNqeIethg1HfGE50MufBCWTWO92D92atqACBIep2/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="213" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ0SvS2poy_uhHz_FrZ_rlbHehlC7NkyUFDxCNnLV6JCTCPkOWQFg05sCw9aeUSl8rBKhC5ewzh7Rsdt8cw-hxyqmAGdkYsJa6WSw-bNqeIethg1HfGE50MufBCWTWO92D92atqACBIep2/s320/3.png" width="320" /></a></div>
<br />
So far this technology mix is working well for us, and we're getting very positive feedback from our users.<br />
<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-9274430747718332812012-09-20T14:48:00.000-04:002012-09-20T14:49:42.036-04:00Consistency testsSUnit tests are a fundamental part of how I write code. I always wonder how people that don't develop with tests know when their code is ready. Perhaps they leave that to their end users.<br />
<br />
I'm most cases my SUnit tests check for specific values, your typical...<br />
<blockquote class="tr_bq">
<span style="font-family: Courier New, Courier, monospace;">self assert: anObject value = 'what I expect'</span></blockquote>
...But there are a couple of projects that I've worked on where checking for a state of an entire object was helpful. These are not typical SUnit assertions, since creating the test ahead of time is not practical. Instead, they are consistency tests: making sure that the state of an object has not changed.<br />
<br />
The first project was <a href="http://www.stic.st/conferences/stic12/stic12-abstracts/pdf4smalltalk-based-report-framework/">Report4PDF</a>, a simple VW reporting framework that uses <a href="https://gitorious.org/pdf4smalltalk">PDF4Smalltalk</a> (<a href="http://www.freelists.org/archive/pdf4st/">mailing list</a>)<br />
<br />
Adding tests for simple actions didn't add much value, since the challenge of a report tool is getting all the layout definitions to work well together; it's the net result that mattered, not the individual outputs. Those were well tested in the PDF4Smalltalk SUnit tests.<br />
<br />
For Report4PDF, after I manually checked a report I wanted to make sure that the output did not change. As more complex reports were added, the simple reports acted as the regression tests. Edge cases were the most interesting, and most of those were found in real world use. I simply did not have the imagination to create the strange scenarios found in the wild. So, an anomaly would surface in production, I'd build a test report that had the same problem, fix it, and add the corrected report check to the test suite. <br />
<br />
Stored data consists of both a diagnostic display string and a byte array of the rendered PDF document. The diagnostic string represents the low level data sent to PDF4Smalltalk and rarely needs to be updated. The PDF byte array needs to be rebuilt each time a material change is made to PDF4Smalltalk.<br />
<br />
Report4PDF tests are in the Report4PDF-test package and coded in R4PReportTest. Reports methods are prefixed with 'example', like...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">exampleAlignCenter</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>" self new exampleAlignCenter saveAndShowAs: 'exampleAlignCenter.pdf' "</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| report | </span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report := R4PReport new.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report businessCard.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report traceToTranscript. </span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report page grid section origin: 10 @ 10; width: 100; height: 100; border: 1; align: #center; string: 'center align'.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^report</span><br />
<br />
...which produces the output...<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSNX3TCIHBC8qHFn8AcdWmj6qSxdjrXUi7nIq5zuYqp7W0joRPA3wN7HTIAOSWKW-trlIjCydjI3Qn-4k29-giQz8HR4MFo3QQJZqrZwGJKe9cZSs7tg7VIGGmsoxNhBFnWR9qKdR88XOf/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="241" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSNX3TCIHBC8qHFn8AcdWmj6qSxdjrXUi7nIq5zuYqp7W0joRPA3wN7HTIAOSWKW-trlIjCydjI3Qn-4k29-giQz8HR4MFo3QQJZqrZwGJKe9cZSs7tg7VIGGmsoxNhBFnWR9qKdR88XOf/s400/1.png" width="400" /></a></div>
<br />
...#createTestContentsPrintOutput: is used to create an output content method...<br />
<span style="font-family: 'Courier New', Courier, monospace;">outputAlignCenter</span><br />
<br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> "Generated on February 26, 2012 4:22:43 PM"</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^'Report</span><br />
<span style="font-family: Courier New, Courier, monospace;">page width: 252</span><br />
<span style="font-family: Courier New, Courier, monospace;">page height: 144</span><br />
<span style="font-family: Courier New, Courier, monospace;">margin: #(0 0 0 0)</span><br />
<span style="font-family: Courier New, Courier, monospace;">layout: 0 252 144 0</span><br />
<span style="font-family: Courier New, Courier, monospace;">font: #Helvetica<span class="Apple-tab-span" style="white-space: pre;"> </span>font size: 10</span><br />
<span style="font-family: Courier New, Courier, monospace;">page number pattern: ''<page>''</span><br />
<span style="font-family: Courier New, Courier, monospace;">page total pattern: ''<total>''</span><br />
<span style="font-family: Courier New, Courier, monospace;">layout pages: 1</span><br />
<span style="font-family: Courier New, Courier, monospace;">---</span><br />
<span style="font-family: Courier New, Courier, monospace;">page width: 252</span><br />
<span style="font-family: Courier New, Courier, monospace;">page height: 144</span><br />
<span style="font-family: Courier New, Courier, monospace;">maximum Y: 144 (page height - footer)</span><br />
<span style="font-family: Courier New, Courier, monospace;">output parts: 85</span><br />
<span style="font-family: Courier New, Courier, monospace;">0 @ 0 line: 252 @ 00.5</span><br />
<span style="font-family: Courier New, Courier, monospace;">0 @ 10 line: 252 @ 100.5</span><br />
<span style="font-family: Courier New, Courier, monospace;">...</span><br />
<br />
<span style="font-family: Courier New, Courier, monospace;">9.5 @ 110 line: 110.5 @ 1101</span><br />
<span style="font-family: Courier New, Courier, monospace;">10 @ 110 line: 10 @ 101</span><br />
<span style="font-family: Courier New, Courier, monospace;">#(10 0 0 -10 34.155 18.215) center align'</span><br />
<br />
<br />
...and #createTestMethodHexString: is used to create the byte array of the PDF document...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">pdfAlignCenter</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> "Generated on April 20, 2012 7:35:33 AM"</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>^'255044462D312E330A25E2E3CFD30A312030206F626A0A3C3C092F50726F6475636572202850444634536D616C6C74616C6B20312E322E3529093E3E0A656E646F626A0A322030206F626A0A3C3C092F54797065202F436174616C6F670A092F...660A3937370A2525454F46'</span><br />
<br />
...finally, #createTestMethodPrintOutput: is used to create the SUnit test method which builds the example output and checks the result. First, the output string...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">testOutputAlignCenter</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> "Generated on February 26, 2012 4:22:53 PM</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> ( self new createTestContentsPrintOutput: #exampleAlignCenter )</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> ( self new exampleAlignCenter saveAndShowAs: 'exampleAlignCenter.pdf' ) " </span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| report |</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report := self exampleAlignCenter.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report buildPDF.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self assert: report printOutput = self outputAlignCenter.</span><br />
<br />
<br />
...and then the PDF array...<br />
<br />
<span style="font-family: Courier New, Courier, monospace;">testPDFAlignCenter</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> "Generated on February 26, 2012 4:22:49 PM</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> ( self new createTestContentsHexString: #exampleAlignCenter )</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span> ( self new exampleAlignCenter saveAndShowAs: 'exampleAlignCenter.pdf' ) " </span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| report |</span><br />
<span style="font-family: Courier New, Courier, monospace;"><br /></span>
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>report := self exampleAlignCenter.</span><br />
<span style="font-family: Courier New, Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self assert: (report byteArraySUnitAs: 'testAlignCenter.pdf') asHexString = self pdfAlignCenter</span><br />
<br />
Class side convenience methods are available to rebuild all the output and test methods. Handy when PDF4Smalltalk changes.<br />
<br />
----<br />
The other project is the domain model of the application we're building at work. It's the same idea: while developing we write SUnit test that check for specific values. Typically this requires us to build complex domain resources. Once these are built, and we've checked the model manually, we add a 'capture' (the word 'snapshot' was already used in domain code) of the domain object's state that records all the domain attributes in an array, and stores the array in a data method.<br />
<br />
How the data is stored is not that important. Most large Smalltalk applications I've worked on had some kind of meta data for domain objects which can be used to generate a data string.<br />
<br />
What was interesting was how used the capture data vs. the regular SUnit tests. Normally, we want the tests to stop when an assert fails, but for captures we wanted the test to continue and have it generate a 'capture report' of which values were different. That's because a simple change, like adding a new domain attribute, would cause almost every capture for that domain class to fail.<br />
<br />
After some trial and error, we have this workflow...<br />
<br />
<ul>
<li>if the capture only contains new or deleted attributes, rebuild the capture array, since none of the old data changed</li>
<li>if any capture data change, generate a capture report (stored as a 'report' prefixed method) and continue</li>
<li>if, however, a capture report already exists, cause an assert to fail</li>
<li>if a capture report is generated, open a browser on the method</li>
<li>if any capture reports are created, a final #assertNotCaptureReports will cause a failed assert</li>
</ul>
<div>
Having the capture test stop if an existing capture report is found allows us to selectively diagnose data issues. We've also added a button to the SUnitTool toolbar which rebuilds all the data captures. Handy when attributes are changed, which is almost daily. </div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdHHVINoIeVjzhoEYXMim7bqhxB4dLlvSSi7cyNqPGyG1bazUgwTByEfNknmgjhgBMB-jRQQg6b6Tum7x_z_a_9fE3b6006Szu5dDlRV74knxGdroMAbWIVe5A6Cb1gsh-qWe71XSSuRy/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdHHVINoIeVjzhoEYXMim7bqhxB4dLlvSSi7cyNqPGyG1bazUgwTByEfNknmgjhgBMB-jRQQg6b6Tum7x_z_a_9fE3b6006Szu5dDlRV74knxGdroMAbWIVe5A6Cb1gsh-qWe71XSSuRy/s1600/2.png" /></a></div>
<div>
<br /></div>
<div>
On a side note: I've been at HTS now for four months, spending long hours learning and updating a 15 year old framework that was written with somewhat esoteric design patterns. I see now how lucky I've been over most of my Smalltalk career, mostly working on code that I either created myself, or developed with a team that shared common development ideals. James Robertson has a <a href="http://www.jarober.com/blog/blogView?showComments=true&printTitle=IM_93:_Common_Pitfalls_(AAC)&entry=3524679928">good podcast on the topic of Common Pitfalls</a>. I think I have examples of everything he and Dave Buck talked about, plus some great ways to <i>not</i> interface with GemStone (and I now loathe lazy initialization, especially when deeply nested and combined with silent exception handling).</div>
<br />
<br />
<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-33757643995839319502012-04-12T14:30:00.004-04:002013-02-24T11:42:52.157-05:00Opening a Seaside view from VWI spend a good chunk of my time working on adding web interfaces to legacy Smalltalk applications, both in VW and VA. The larger project is in VW and is built with a big in-house framework. A couple of years ago I wrote a VW windowSpec to Seaside component builder which used the framework metadata to bind Seaside components to domain objects. It worked, but required too much of an investment to fully deploy.<br />
<br />
So, we took another look at what clients needed from a web interface and decided that a 'portal' model was a better fit: a limited access web site useful to a subset of users. It is implemented with a Seaside image that has no domain objects, just parsed XML data from a RESTful GS interface. Seaside sessions share one GemStone session and rely on the application framework for login and security. It works nicely.<br />
<br />
One of the views is table display of competitors by project, showing who is bidding on which section of the project, their bid status (won / lost / undecided), the estimated bid amount, and so on. This particular display was a challenge to do in the VW framework because it only supports a fixed number of columns in a table, and does not allow for in-cell editing (there may one day be support for the dataset widget).<br />
<br />
We still wanted to make this display available to the VW users and the Seaside table looked nice. The solution was to launch a browser showing the selected table from a VW button press. This hybrid user interface (VW + web browser) may allow for a smoother incremental deployment of a full web based interface vs. an expensive and disruptive big bang approach.<br />
<br />
When the 'Show table' button is pressed, a session token is saved on the logged in 'user' object in GemStone (each user has their own 'user' instance which handles things like application login and security). The oop of the saved session token is passed as a URL parameter (ExternalWebBrowser open: '...?start=12345678), and the token contents (user oop and timestamp) are checked to see if it is valid: oop of the user object must match the user object that contains the token & the timestamp of the token must be within a few seconds. If it matches, the token is cleared and a Seaside session is established. Each token can only be used once, for a short time and to access an internal web site; seems reasonably safe.<br />
<br />
The token also contains display information which the Seaside image uses to build the table; a user presses a button and a browser opens on the expected table. Changes are stored in GemStone, so both the browser and the VW client see the same data.
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCK8EJssrwky2yIxgt3oyvDrzQ1e4ASyW80HyIZBBLOxcbnP6Lj9wF-vUdatI3xcng464jCT3YY2qDZtaR8DtNeJTBfBA0DCoSt4CpdK1kuPPvlJL-tNGUrzL8GypYW5sr7RI9D0Tyjbsw/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="107" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCK8EJssrwky2yIxgt3oyvDrzQ1e4ASyW80HyIZBBLOxcbnP6Lj9wF-vUdatI3xcng464jCT3YY2qDZtaR8DtNeJTBfBA0DCoSt4CpdK1kuPPvlJL-tNGUrzL8GypYW5sr7RI9D0Tyjbsw/s320/1.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Flyover components are rendered to display attributes and allow for updates. Users can change the status of a bid by pressing 'won', 'lost' or 'unknown' buttons in the flyover component. This is a quick way to edit the bid state vs. the VW based multi-window, multi-click sequence. I tried to use Seaside's jQuery tools to build the onMouseOver and onMouseOut scripts, but I found it simpler to just write the few lines I needed.<br />
<br />
This script, as passed to table data's #onMouseOver: , positions the hidden flyover component (aFlyoverId) to the left and top of the cell under the mouse (aCellId), and then shows it. I was able to do this with Seaside jQuery code, but I could not figure out how to add the cell width to the flyover's 'left' position.<br />
<br />
<br />
<blockquote class="tr_bq">
onMouseOverFlyoverId: aFlyoverId cellId: aCellId<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>^'<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>$("#', aFlyoverId ,'").css("top",$("#', aCellId ,'").position().top);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>$("#', aFlyoverId ,'").css("left",$("#', aCellId ,'").position().left + $("#', aCellId ,'").width() + 8);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>$("#', aFlyoverId ,'").show();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>$("#', aCellId ,'").css("background-color","#F2F7FA");<br />
'</blockquote>
<br />
<br />
The flyover component has its own #onMouseOver: script to keep it visible when the mouse moves away from the cell and over the flyover component. <br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9HpoIhvhsQYUfXpvzjuksaw8zjP_cLIX0aRvzlFozYQti4ExrL3rBd-sazXB-sh1eceVMhL_oF8L8dvaeqUVMBra79zjaUbcIk71fB5HLPEL_4_Kvq6JHZyaplvHNlZr9cZCdX3O_RGsV/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="292" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9HpoIhvhsQYUfXpvzjuksaw8zjP_cLIX0aRvzlFozYQti4ExrL3rBd-sazXB-sh1eceVMhL_oF8L8dvaeqUVMBra79zjaUbcIk71fB5HLPEL_4_Kvq6JHZyaplvHNlZr9cZCdX3O_RGsV/s320/2.png" width="320" /></a></div>
<br />
<br />
Views that show a consolidated view of objects, like the competitor table, are good candidates for the initial web interface. The XML based data gathering from GS is quick, since no domain objects are faulted to the client, and the display options are more flexible. Whereas VW fat client's detailed object level views are better for fine grain data.<br />
<br />
The next step is to merge the windowSpec Seaside component builder with the RESTful web portal. Not hard to do, but we'll need to see if there is client interest.<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-46969022933502925922012-03-08T10:59:00.001-05:002012-03-08T10:59:33.462-05:00Who needs objects?Dave Thomas has said that the <a href="http://channel9.msdn.com/Blogs/Charles/SPLASH-2011-Dave-Thomas-On-Modern-Application-Development">object abstraction is too complex for the majority of programmers</a>. Most business software is CRUD with a bit of business logic mixed in. And it can scale by building loosely coupled systems (works for the internet, eh). Dave is a giant; he sees far. I think he's right.<br />
<br />
So what does this mean to an object evangelist like me? Probably not much. That vacant look on most people's faces when you try to explain objects says it all. If it does not address an immediate need, object abstraction is noise.<br />
<br />
At the <a href="http://www.smalltalk.ca/">Toronto Smalltalk User Group</a> meetings we sometimes have one or two students from Ryerson University. By attending they've already indicated that they're interested in more than the generic C syntax procedural stuff they learn in school. Joshua Panar and Dave Mason, the two profs that sponsor our group and use Smalltalk in their OO course, have said that getting the regular students out is a challenge. They're not interested. They don't see it as improving their education or job prospects. Suggesting that they should broadening their horizons falls on deaf ears.<br />
<br />
There are two types of programmers: the toolsmiths (abstractionists) and the tool users (constructionists). Smalltalk developers seem to all be abstractionists. It is natural of us to extending our environment. Want a framework? Build it. Need a new compiler behavior? Add it. It's easy; it's common for us, yet unheard of by others.<br />
<br />
Most programmers are constructionists. They have a job building and maintaining business applications. As Smalltalkers we ask ourselves: how can we get these programmers to use Smalltalk, to see how much more productive and enjoyable our environment is? The answer, I believe, is to reduce barriers to entry.<br />
<br />
How to do that? Here are my wishful thinking answers...<br />
<br />
<ul>
<li>Merge the dialects (ya, I know: unlikely). Selecting a dialect as the first step in exploring Smalltalk is a big problem. You need to know a lot to make a good decision, at the point where you know little. Yes, the VW & Digitalk merger was a bust. But that was another time. But I can dream...</li>
<li>Use a common online forum.
The Balkanization of the Smalltalk community is a problem. Think of how hard it is for a Smalltalk curious person to find information. If we at least used a common forum, like <a href="http://stackoverflow.com/">Stack Overflow</a>, it would be easier to find cross dialect posts, and it would be more visible to the larger developer community. I'll advocate for it again at the the upcoming STIC conference, but I must be turning into a cynical cranky old fart, because I don't think there will be a change.</li>
<li>Support simple scripting. I know it's been done in various ways (S# was cool), but we should be able to point to a simple script tool for people to try. If there is a good option out there, consider this: I'm a Smalltalk cognoscenti, and I'm not aware of an option that does not require firing up an image. What does that say about how well we get the word out?</li>
<li>Start with prototype objects. Self and javascript got it right. It is easier to explain objects if you can defer talking about classes, and where the value of classes is discovered as a useful pattern.</li>
<li>Make Smalltalk IDEs rock. I know the Smalltalk vendors and volunteers have done a great job with the resources available, but VisualStudios and Eclipses of the world are slick by comparison.</li>
<li>Examples. Lots of examples. It would be great if we could point to real applications that people could fire up, test and explore. And templates; wizard driven templates to help build new applications, like those found in MS Access. Need an application to track students? Here's an example and / or a tool to help you get started. If nothing else, <b>make it easy to get started</b>.</li>
</ul>
<br />
Yes, abstractions are hard. But abstraction allows you to do things that would be far too difficult and expensive otherwise. Knowing how to think in abstract terms is a powerful skill that will make you a better technologist. It is our job, as those that understand this, to make it self evident to others.<br />
<br />
<span style="font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com10tag:blogger.com,1999:blog-2691799628167753841.post-8373113546795033792012-01-31T08:16:00.002-05:002012-01-31T08:17:05.039-05:00VW Code CoverageThe past few days I've been working with the code coverage extensions to SUnitToo in VW (SUnitToo(verage) and SUnitToo(lsoverage). It is an impressive tool. And the metrics accurately reflected the way I developed some packages. Our in-house issue tracker and patch manager had tests added after the fact, with a resulting code coverage of 30%. The Report4PDF package, the PDF4Smalltalk based report writer I'm working on, was developed from the start with SUnit tests, with a code coverage of 65%.<br />
<br />
At first I thought that 65% seemed a bit low, since all the code has associated tests. But the code coverage is smart: it does not just measure method hits, but which paths within a method are exercised. In many cases these were exception and error message branches; the paths less taken. But some cases should have been tested. These were very narrow special cases that I had simply not thought of. The code looks OK, and I'm confident that it will work, but I won't release it without an SUnit test.<br />
<br />
If feels like I've been given a flashlight to see the dark nooks and crannies of my code.<br />
<br />
I also like the idea of using the path count (the denominator in the code coverage number) as an application complexity metric. It could work well when used with number of classes, number of methods, and average method size. I wonder if 'number of paths per method' would be useful.<br />
<br />
It is an impressive tool. Thanks to all who made it available.<br />
<br />
<span style="font-family: Georgia, 'Times New Roman', serif; font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com1tag:blogger.com,1999:blog-2691799628167753841.post-85589293803451593922012-01-08T19:05:00.004-05:002012-01-09T07:06:47.055-05:00PDF Report and the Law of DemeterI'm finishing a small project which uses Christian Haider's <a href="http://pdf4smalltalk.origo.ethz.ch/">pdf4smalltalk</a> to build report output using a Seaside influenced coding style. A report with a header, text and footer would be coded as...<br />
<br />
<br />
<blockquote class="tr_bq">
<span style="font-family: 'Courier New', Courier, monospace;">| report |<br />report := PRwReport new.<br />report portrait.<br />report page: [:page |<br /><span class="Apple-tab-span" style="white-space: pre;"> </span>page header string: 'This is a header'.<br /><span class="Apple-tab-span" style="white-space: pre;"> </span>page text string: self someText.<br /><span class="Apple-tab-span" style="white-space: pre;"> </span>page footer string: 'This is a footer'].<br />report saveAndShowAs: 'TestText.pdf'.</span></blockquote>
<div>
<br /></div>
<br />
The tool supports the usual report output options, like fonts, alignment, tables, images, bullets and so on.<br />
<br />
I've built a couple of other Smalltalk report frameworks over the years. One used Crystal Reports for the layout with configurable data gathering, and another (much better tool) that used <a href="http://www.totallyobjects.com/">Totally Object's</a> Visibility (I did a <a href="http://dl.dropbox.com/u/5602924/TSUG_Smalltalk.pub">presentation</a> on that one at Smalltalk Solutions 2004). Both of those used a data + layout spec model, which, with the benefit of hindsight, was not be best choice. It was a challenge to keep the code and layout in sync. Maintenance was painful. For PDF Report I opted for Seaside's 'paint the content on a canvas' pattern. It is working nicely (I'll be presenting the details at <a href="http://www.stic.st/conferences/stic12/">Smalltalk Industry Conference in Biloxi</a>). <br />
<br />
Here's the part that got me thinking about how nice objects are and the <a href="http://en.wikipedia.org/wiki/Law_of_Demeter">Law of Demeter</a>... when building a report output, you have to deal with coordinating the size and position of layout components on the page. Do you give the responsibility to the page, or do you have the layout objects find their own place? I opted for a 'builder': it knows how much space is available on a page and which layout objects need to be processed.<br />
<br />
The interesting part was in deciding how much the builder needed to know about each layout. The first few iterations were rudimentary: each layout had a calculated height (word wrapped text with a selected font) and the builder would output as much as would fit on one page, then trigger a page break and continue on to the next page.<br />
<br />
But that did not work with tables, since each row could have some cells that spanned pages. The builder could not blindly trigger a page break on a tall cell, since the next cell would be on the previous page. The table, row and cell had to communicate layout information to the builder, with the cell width and height dependent on neighbouring cells. And, to make things especially interesting, tables can be nested and cells can span rows and columns, like this...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMHAyfp-EgGsExlIvbbW66Lsan3J6kPvObWHfznKlzAgejBvd94ad7uft1_UW4LFhxyIL-cG8V3D8sJ1Nu9wZSd3N3zGs45lV3js18ljrYFUtWnyPVTL8U8elq9T8TzkvTAxY1vN1brUzq/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="105" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMHAyfp-EgGsExlIvbbW66Lsan3J6kPvObWHfznKlzAgejBvd94ad7uft1_UW4LFhxyIL-cG8V3D8sJ1Nu9wZSd3N3zGs45lV3js18ljrYFUtWnyPVTL8U8elq9T8TzkvTAxY1vN1brUzq/s320/2.png" width="320" /></a></div>
...and this...<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQtVB26hJQ83F-EWA293jo6iU1SEpK2f3CQO2PUKVbIxqMAKtdN8WU6M8jF27PHwXchpcL_HJHueRMPBwx1pVh0r1k7R9q15UM_uHcTRb1COaDjt6GRDU338cvLm8o5Zd-F-o_MOh8KKPw/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="70" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQtVB26hJQ83F-EWA293jo6iU1SEpK2f3CQO2PUKVbIxqMAKtdN8WU6M8jF27PHwXchpcL_HJHueRMPBwx1pVh0r1k7R9q15UM_uHcTRb1COaDjt6GRDU338cvLm8o5Zd-F-o_MOh8KKPw/s400/1.png" width="400" /></a></div>
<br />
<br />
Each time I added a new layout mix to the SUnit tests I had to rethink what each object knew. After several iterations a pattern emerged: the less the builder knew about the layout objects, the better. And as the builder got dumber, its code got simpler and new layout mixes just worked. A tricky part was in sequencing the layout calculation for nested objects: a cell's height is dependent on the row's height, but the row's height is the maximum of it's cell heights.<br />
<br />
Once the calculation sequence was correct, each layout object was able to answer it's layout values: position, margin and padding. The builder could ask if a layout could fit in the remaining space on a page without knowing what the layout objects was (text, table, bullet, image or line) and could create a new physical page without knowing how a layout object would be split. Now each refactoring cycle starts with me asking myself: how can I reduce what the builder needs to know? The latest version is much cleaner than the first. It's nice to apply well known object design rules and see real results.<br />
<br />
Still a lot of work to do, but I'm looking forward to showing it at the conference. And, if it's good enough, it will be added to the VW public store (long term plans are to port to other Smalltalk dialects).<br />
<br />
<span style="font-family: 'Trebuchet MS', sans-serif; font-size: x-small;">Simple things should be simple. Complex things should be possible.</span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-72771870369086338562011-11-15T10:39:00.001-05:002011-11-21T10:36:12.272-05:00Smalltalk head spaceAt the last Toronto Smalltalk User Group meeting we had a basic introduction to Seaside. It had been requested by some of the members, especially by the two profs that sponsor our group at Ryerson University. They teach an OO course with a Smalltalk component and felt it would be of interest to their students. A few did show up and the feedback was very positive. A bit surprising, given how basic the demo was (<a href="http://dl.dropbox.com/u/5602924/Seaside_example.pdf">here is a link to the PDF</a>), but it got me thinking about how easy it is to get out of touch with how other people see things. There is something to be said about baby steps.<br />
<br />
After the talk we spent a while talking about the sad sate of computer science education; how it's become a C syntax programming training school. Way back in my day (early 80's) there was no dominant computer language. Intro to comp. sci. was taught in FORTAN. We learned BASIC, PL/I, COBOL, SNOBOL, Lisp, 360 Assembler, Prolog and Smalltalk. My OS course was taught as a history lesson, explaining why and how each OS concept was introduced, and why some were abandoned. There was a big focus on 'why', not just 'how'. That does not seem to be the case today.<br />
<br />
During the conversation, I talked a bit about how selling Smalltalk can be a challenge. Smalltalk is different. If all you know is C and Java, it's really different. Why learn something different if it will not help you get a job? I made the case that learning Smalltalk helps you learn how to think in objects. A perspective that may be helpful when dealing with the other object hybrid languages (it would also be good to learn Lisp, Prolog and assembler). You can't help thinking that a Java-centric education gives students only one tool: a hammer, and they then view every task as a nail.<br />
<br />
But how do you communicate the value of a "Smalltalk head space"? A world where everything is a live object that you can see and change. A place where delegating behaviour to Integer and String makes perfect sense; where the language is minimalist and the libraries add the complexity; where working with a debugger feels natural. And how do you explain how sending messages is different than calling a function? The mechanics are simple enough, but the mental model is harder.<br />
<br />
I see functions as a handing off of total control of the world to something else. Within a function you change whatever data you need to. The world is a large, porous bit universe that you manipulate. Whereas sending a message is asking another object to do something for you. You don't think about tickling the bits of another object. You do your job, send messages to other objects, and maybe answer something. You work in a local scope, not the whole world.<br />
<br />
Every professional Smalltalk developer I know can tell talk about their "aha" moment, the point where object-think made sense. In my case it was a course at The Object People where Paul White was explaining how to model a transfer between a chequing and savings account: asking either account to do the transfer smelled wrong, so instead you could model the transaction. Really? You can do that? A 'transction' can be an object? Cool. A more whimsical example is modelling the milking of a cow. Do you ask the cow to milk itself? Do you ask the milk to un-cow itself? No. You model a farmer to do the milking 'transaction'.<br />
<br />
The problem is that those "aha" moments take time to learn. And who has time for that? As Dave Thomas said in his <a href="http://channel9.msdn.com/Blogs/Charles/SPLASH-2011-Dave-Thomas-On-Modern-Application-Development">recent SPLASH video interview</a>, the complexity that we've added to the development tools is not always a benefit to someone building a simple application. Do you really need to know about objects if a BASIC program will do? Is learning about classes and instances to big a hurdle? Would a prototype object model be easier to learn? I don't know, but we really have to find a better way to communicate the up side of using Smalltalk. Waiting for each student to experience their own "aha" moment is not good enough.<br />
<br />
<span class="Apple-style-span" style="font-size: x-small;"><i>Simple things should be simple. Complex things should be possible.</i></span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com1tag:blogger.com,1999:blog-2691799628167753841.post-7643991271852164842011-09-12T11:48:00.000-04:002011-09-12T11:49:04.698-04:00XML RESTful Seaside interface to legacy systemI'm mandated with creating web interfaces to two legacy frameworks, one written in VA and the other in VW.<br />
<br />
For the VW framework I started with a full application implementation; a Seaside interface that duplicated the entire application interface by parsing VW window specs and building Seaside components. But for both the VA and VW systems there is also a need for a 'portal' web site, one that exposes a limited view of the application but is intended for a larger user base.<br />
<br />
Both systems were designed over a dozen years ago, using GemStone and a fat client. Grafting a multi-user Seaside interface to the fat client designed for a singe user was not going to work. That's why the VW 'full user' implementation uses one 200MB VW image per Seaside session, a deployment model that was not an option for the portal: too many users (a resource issue), too much exposed (a security concern), too integrated (a maintenance problem).<br />
<br />
To work around these constraints I've implemented an XML based domain interface, where a Seaside image can gather the data it needs to render using a RESTful interface to GemStone. There is no domain model in the Seaside image, just components that know what part of the domain they represent. With GemStone this works particularly well since it eliminates the need to fault objects into the client. It's a common technique with GS, popularised by James Foster, to build strings on GS, fault them to the client, then parse and display the content. Some displays, like lists of objects, can improve their performance from tens of seconds to sub-second.<br />
<br />
Each Seaside component knows which dedicated server method to use. Since the portals have a limited scope there are not that many XML data methods. I would not use this approach if I expected the portal application scope to increase significantly.<br />
<br />
All of this was made easier by thinking of the final rendering as a limited set of display patterns: lists, tables, trees, image and a single domain object. The XML has tags that identify the display pattern: <list> <table> <tree> <image> <domain>. These are used to build 'XmlObject' subclasses for each pattern. This makes accessing and debugging data easier than referencing the parsed XML code directly in the Seaside components.<br />
<br />
Parsing XML is very different between VA and VW. Wrapping the results in my own XML node objects made it easy to port my code between dialects. I just needed to abstract out the XML parser references. Predefined tags, like 'oop, objectclass, text' are stored in instance variables. Any other tags are stored in a dictionary. The Seaside component would expect certain tags to be present and will trigger an error otherwise.<br />
<br />
Some examples...<br />
<br />
<b>domain</b><br />
- only attributes the Seaside component needs are included<br />
- used in the list, table and tree patterns with minimal attributes; if selected the oop is used to get whatever else is needed<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><domain></span><br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>461206257</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><objectclass>AlaItemWorksheet</objectclass></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><text>ABTP Lab Results - D - DEN</text></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><icon>AlaDataEntry.ico</icon></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><label>ABTP Lab Results - D - DEN (TAB [Final Effluent])</label></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"></domain></span><br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm8f7PuCXDD8syukdH2WsK3yXBd6Up4F_RP7cwooTHLrqN54FSxmxGG43f7OREkGMCDZjDkJx_HOtbB27-1iyioknwQwFWf9N-CmX4dEd4aIppoUPvXds3s_LHn7rsDBYbUkCSWxyljS14/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="108" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm8f7PuCXDD8syukdH2WsK3yXBd6Up4F_RP7cwooTHLrqN54FSxmxGG43f7OREkGMCDZjDkJx_HOtbB27-1iyioknwQwFWf9N-CmX4dEd4aIppoUPvXds3s_LHn7rsDBYbUkCSWxyljS14/s400/1.png" width="400" /></a></div>
<br />
<br />
<b>list</b><br />
- stored as named lists in the collection of attributes for a domain node<br />
- each element of a list is another domain node, with enough information to be displayed (label, icon) and with the domain object oop in case it gets selected<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><list type="actions"></span><br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><domain></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>291900165</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>...</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><domain></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>291913433</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>...</span><br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzhg9UTUeG_tXW66qOHbQYXciqXL5BrVkLx2g5TPCUwfRCYbCGcIMS2pXfsgGEubGJ41KJjDU5rTnaNGaJKjPT-rClAj4MaETFmV4tttxI7-l_oK4LO2ma8UfF5eajx7_GD4ywQVkXh06C/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzhg9UTUeG_tXW66qOHbQYXciqXL5BrVkLx2g5TPCUwfRCYbCGcIMS2pXfsgGEubGJ41KJjDU5rTnaNGaJKjPT-rClAj4MaETFmV4tttxI7-l_oK4LO2ma8UfF5eajx7_GD4ywQVkXh06C/s1600/3.png" /></a></div>
<br />
<br />
<b>table</b><br />
<br />
- added tags for header and styles. The style contents is encoded as key, value pairs so that the server code can include style information that a generic table component will render<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><table></span><br />
<br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><header></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><data></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><value>ABTP Lab Results - D - DEN</value></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span></data></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br /></span><br />
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj37-e8OxNjSfzr8PR4VsQkb5MzNJjzSSvYHVqx1chl-gaJIjwltM7QqqekEt4jP0_eqxhn4pyKuRc-eqEFMGDEZOI_1P2vcK8nr2nt0xINe9mgSRttSoED-aKrZnS-jDlc0vANWWcJsqEs/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="199" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj37-e8OxNjSfzr8PR4VsQkb5MzNJjzSSvYHVqx1chl-gaJIjwltM7QqqekEt4jP0_eqxhn4pyKuRc-eqEFMGDEZOI_1P2vcK8nr2nt0xINe9mgSRttSoED-aKrZnS-jDlc0vANWWcJsqEs/s320/2.png" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<br />
<b>tree</b><br />
- each tree node knows how to get its sub-tree<br />
- sub-trees are retrieved and cached when a tree node is expanded<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><list type="productSet"></span><br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><domain></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>199226369</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><objectClass>INproductSet</objectClass></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><text>By Function</text></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><hasSubtree>true</hasSubtree></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><hasProducts>false</hasProducts></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span></domain></span><br />
<br />
<div>
<br /></div>
<div>
A Seaside component is created for each tree node which reads the 'hasSubtree' attribute and, based on a configuration, gets the nested list of domain nodes when expanded.</div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiw2m-0C2yC_p45aOunSk8R-_ToAxw2-5gk_haHymsf3zEWXiRuYaL-a2yR82owhni_vwzAGkWaxPA3F3QxUruUwmPDEl7B-HHb4CvAesQTolfkZgD6tRFJu3SvTCIPqTriHJ1fs78KX2BG/s1600/5.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiw2m-0C2yC_p45aOunSk8R-_ToAxw2-5gk_haHymsf3zEWXiRuYaL-a2yR82owhni_vwzAGkWaxPA3F3QxUruUwmPDEl7B-HHb4CvAesQTolfkZgD6tRFJu3SvTCIPqTriHJ1fs78KX2BG/s1600/5.png" /></a></div>
<br />
<br />
<br />
<b>image</b><br />
- for the VA implementation, images are stored as byte arrays on GS<br />
- for the VW, the images are stored as server paths<br />
- in each case, the image node needs to answer a byte array that can then be rendered<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><domain></span><br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>249729481</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><objectClass>AlaSiteMapImage</objectClass></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>...</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><image></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><oop>249728229</oop></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><mimeType>image/gif</mimeType></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span></image></span><br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoRj-qFdtYjDt44f4huDGuEH_hrW_SvbKk2C9FH_dFpN4R1esu56itTkw-8ChOOe833WBezW1oNmZnJCsCKRA9ig8mwXJfa3Hue2yZVjRIKI8P5KN04YpqO0JqHKwLOIDEevcOU5QaFvJg/s1600/4.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="61" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoRj-qFdtYjDt44f4huDGuEH_hrW_SvbKk2C9FH_dFpN4R1esu56itTkw-8ChOOe833WBezW1oNmZnJCsCKRA9ig8mwXJfa3Hue2yZVjRIKI8P5KN04YpqO0JqHKwLOIDEevcOU5QaFvJg/s320/4.png" width="320" /></a></div>
<br />
<br />
Building the XML on the server is done with a 'canvas' object which wraps tags around indented nested content. Formatting of the XML content is a debugging convenience.<br />
<br />
To add a domain object...<br />
<br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">aCanvas </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>addDomain: anItem </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>text: anItem product id</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>with: [</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>aCanvas add: 'description' put: anItem product description.</span><br />
<br />
<div>
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>...</span></div>
<br />
<br />
To add a list...<br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">aCanvas </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>addListNode: productSet</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>type: 'productSet'</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>with: [</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>collection do: [:each | self buildXmlProductSet: each on: aCanvas]].</span><br />
<br />
<br />
For both apps a user identifier, in the form of a GS object oop, is included in each data request. Seaside stores the user object oop in the WASession subclass instance variable. Communication between the Seaside image and GS is behind a firewall, so I'm not that concerned about it being monitored.<br />
<br />
RESTful communication works well with my Smalltalk message passing sensibilities. Objects passing messages feels so natural, and debugging is easy since I can record and replay any message. And I don't care what gets changed on the server, as long as the Seaside XML methods answer the same way.<br />
<br />
Although the code is written in both a VW and VA client, it's dialect agnostic; they could be swapped. I stuck with the two Smalltalks because I was replacing a domain model Seaside implementation in each, and there was enough sunk cost to leave that code as is.<br />
<br />
Ideally I would like to host the Seaside sessions from GS with a GLASS deployment. It's a long term option with the VW application, but the VA application has to be hosted on a Windows server, which limits us to 32 bit GS and no GLASS.<br />
<br />
So far testing is going well. The next step is to test this under load and see how many concurrent Seaside sessions one image can handle. We already have a multi-Seaside image dispatcher implementation working with Apache that supports session affinity, so I'm confident that scaling will not be a problem.<br />
<br />
<span class="Apple-style-span" style="font-family: 'Trebuchet MS', sans-serif; font-size: x-small;"><i>Simple things should be simple. Complex things should be possible.</i></span>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-14803931343901024072011-08-10T10:42:00.001-04:002011-08-11T09:05:54.318-04:00Seaside with HTML5 for iPad & iPhoneWe did a demo recently on a iPad showing how, with HTML and some javascript, we could make a Seaside client web site look native-ish. It's impressive what a minimalist layout, large fonts and big buttons can do.<br />
<br />
This is new system development: the client had already seen a simple Seaside 'portal' web site which gave employees access to their shifts, with selected shift details and client information (each shift is a service to a specific client). All done as one integrated system for which they have a VW fat client.<br />
<br />
For the iPad and iPhone we wanted to be aware of the screen real estate, include some device orientation sensitivity and to make sure that all selections were fat figure friendly (I'm a good tester for the 'fat finger' part).<br />
<br />
For device orientation, you can use<a href="http://matthewjamestaylor.com/blog/ipad-layout-with-landscape-portrait-modes"> @media rule in CSS</a>, or javascript, which is what I did. I set up a landscape and portrait DIVs and then used javascipt to hide and show the appropriate one. The layout was simple: in landscape I have a 75% / 25% horizontal split. In portrait right 25% area is rendered below in a wide layout. You can do some cool stuff with orientation. In most cases you don't what the display to flip around too much. But you can set up a image, like a logo, that responds to each small tablet movement on any axle. I didn't use that in the demo, but it looks cool.<br />
<br />
This is the orientation script I used.<br />
<br />
<blockquote>if (window.DeviceOrientationEvent) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>window.addEventListener('deviceorientation', function(eventData) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>deviceOrientationHandler();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}, false);<br />
} else {<br />
makeLanscape();<br />
}<br />
function deviceOrientationHandler() {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>if ( orientation == 0 ) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>makePortrait();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>else if ( orientation == 90 ) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>makeLanscape();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>else if ( orientation == -90 ) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>makeLanscape();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>else if ( orientation == 180 ) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>makePortrait();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}<br />
}<br />
function makePortrait() {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>document.getElementById("tabletLandscape").style.display = 'none';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>document.getElementById("tabletPortrait").style.display = 'block';<br />
}<br />
function makeLanscape() {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>document.getElementById("tabletLandscape").style.display = 'block';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>document.getElementById("tabletPortrait").style.display = 'none';<br />
}</blockquote><div><br />
</div>And this does the funky real time image orientation, with an image with id "imgLogo".<br />
<blockquote>if (window.DeviceOrientationEvent) {<br />
window.addEventListener('deviceorientation', function(eventData) {<br />
var LR = eventData.gamma;<br />
var FB = eventData.beta;<br />
var DIR = eventData.alpha;<br />
deviceOrientationHandler(LR, FB, DIR);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}, false);<br />
} else {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>alert("Not supported on your device or browser. Sorry.");<br />
}<br />
function deviceOrientationHandler(LR, FB, DIR) {<br />
//for webkit browser<br />
document.getElementById("imgLogo").style.webkitTransform = "rotate("+ LR +"deg) rotate3d(1,0,0, "+ (FB*-1)+"deg)";<br />
//for HTML5 standard-compliance<br />
document.getElementById("imgLogo").style.transform = "rotate("+ LR +"deg) rotate3d(1,0,0, "+ (FB*-1)+"deg)";<br />
}</blockquote><br />
To get the screen size, I dug up some code that Lukas had posted on how to get screen size from the browser. It's one of those code snippets that really helped me understand how Seaside gets the browser to trigger callback blocks on the server.<br />
<br />
<blockquote>renderScreenSizeOn: html<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>self screenX isNil ifTrue: [<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>self screenX: 0.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>self screenY: 0.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>script: ('window.location.href="' , html context actionUrl asString ,<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>'&' , (html callbacks registerCallback: [:v | self screenX: v asNumber]) , '=" + screen.width + "'<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>, '&' , (html callbacks registerCallback: [:v | self screenY: v asNumber]) , '=" + screen.height') ].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html text: self screenX printString, ' x ', self screenY printString</blockquote><br />
...which generates the script...<br />
<br />
<blockquote>window.location.href="/Test?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=" + screen.width + "&2=" + screen.height</blockquote><br />
So, I triggered href="/Test?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=4288&2=1143 which Seaside reads and sends '4288' as the #value: to block [:v | self screenX: v asNumber] and '1143' to [:v | self screenY: v asNumber].<br />
<br />
If I edit the URL and change the values in the same session...<br />
<blockquote> ?_s=sOMeODiqoU20GBt4&_k=pbWTZltjW35uekru&1=1234&2=4567</blockquote>..my screen will render the view values: 1234 x 4567. Neat.<br />
<br />
Anyway, it was simpler to just use the request 'user agent' information to redirect to different applications, in the same image, for each device. This also made it easy to test the tablet and phone URLs on a full browser by using the appropriate URL.<br />
<br />
<blockquote><span class="Apple-tab-span" style="white-space: pre;">renderContentOn: html | string | </span><span class="Apple-tab-span" style="white-space: pre;"> </span>string := self requestContext request userAgent.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>(string includesSubString: 'iPad') ifTrue: [^self requestContext redirectTo: self tabletURL].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>(string includesSubString: 'iPhone') ifTrue: [^self requestContext redirectTo: self phoneURL].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>(string includesSubString: 'webOS') ifTrue: [^self requestContext redirectTo: self tabletURL].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>^self requestContext redirectTo: self portalURL</blockquote><br />
I subclasses most of my components with *Tablet and *Phone suffixed classes, which made for easy code reuse and kept all of the device specific code in Seaside.<br />
<br />
The portal required some diagram annotation, so I used javascript to detect figure movement and added more javascript for mouse movement. This way images could be annotated from any device. I started with a canvas ...<br />
<span class="Apple-tab-span" style="white-space: pre;"><br />
</span><br />
<blockquote><span class="Apple-tab-span" style="white-space: pre;"> </span>html div id: 'container'; with: [<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html canvas id: 'sketchpad'; width: 350; height: 350; style: 'border: 1px solid black; background: white';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>with: [html strong: 'Your browser does not support canvas.']].</blockquote><br />
...and then used this script for the iPad & iPhone figure drawing...<br />
<br />
<blockquote>// get the canvas element and its context<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>var sketchpadCanvas = document.getElementById('sketchpad');<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>var context = sketchpadCanvas.getContext('2d');<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>context.fillStyle = 'red'; // red<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>context.strokeStyle = 'red'; // red<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>context.lineWidth = 4;<br />
//Load the image object in JS, then apply to canvas onload<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>var myImage = new Image();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>myImage.onload = function() {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>context.drawImage(myImage, 0, 0, 350, 350);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>};<br />
// create a drawer which tracks touch movements<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>var drawer = {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> isDrawing: false,<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> touchstart: function(coors){<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> context.beginPath();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> context.moveTo(coors.x, coors.y);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> this.isDrawing = true;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> },<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> touchmove: function(coors){<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> if (this.isDrawing) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> context.lineTo(coors.x, coors.y);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> context.stroke();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> }<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> },<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> touchend: function(coors){<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> if (this.isDrawing) {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> this.touchmove(coors);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> this.isDrawing = false;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> }<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> }<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>};<br />
// create a function to pass touch events and coordinates to drawer<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>function draw(event){<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> // get the touch coordinates<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> var coors = {<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> x: event.targetTouches[0].pageX,<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> y: event.targetTouches[0].pageY<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> };<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> // pass the coordinates to the appropriate handler<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> drawer[event.type](coors);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>}<br />
// attach the touchstart, touchmove, touchend event listeners.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>sketchpadCanvas.addEventListener('touchstart', draw, false);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>sketchpadCanvas.addEventListener('touchmove', draw, false);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>sketchpadCanvas.addEventListener('touchend', draw, false);<br />
// prevent elastic scrolling<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>document.body.addEventListener('touchmove',function(event){<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span> event.preventDefault();<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>},false);<span class="Apple-tab-span" style="white-space: pre;"> </span>// end body:touchmove</blockquote><br />
To work on a non-touch browser, I loaded scripts to do the annotation with a mouse <a href="http://www.robodesign.ro/coding/canvas-paint/20090423/">based on this tutorial</a>.<br />
<br />
Setting a background image, which also cleared the annotation, was done with a script<br />
<blockquote>myImage.src = "/files/TSwaFileLibrary/body.png";</blockquote>Saving the image was done with #onClick: script from a button.... the 'toDataURL()' is the interesting bit.<br />
<blockquote>html jQuery ajax<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>serializeForm;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>callback: [:value | self saveImageFile: value]<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>value: (Javascript.JSStream on: 'sketchpadCanvas.toDataURL()')</blockquote><br />
...and... (removing the first 22 characters is a hack to strip out the ''data:image/png;base64,'' MIME declaration). The code is in VW.<br />
<blockquote>saveImageFile: anImageString<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>| writestream string | <span class="Apple-tab-span" style="white-space: pre;"> </span><span class="Apple-tab-span" style="white-space: pre;"> </span>writestream := (self newImageFilename asFilename withEncoding: #binary) writeStream.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>string := anImageString copyFrom: 23 to: anImageString size.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>[writestream nextPutAll: (Seaside.GRPlatform current base64Decode: string) asByteArray] ensure: [writestream close].</blockquote>The demo went well. I'm always surprised how making something pretty makes people think it's better. In this case I just added a new facade on a web site, and it was considered a big thing, yet it was only a few days work. It's like my buddy in marketing says: ya gotta sell the sizzle, not the steak. There's a lesson in there somewhere for Smalltalk.<br />
<br />
(here is a good summary of <a href="http://matt.might.net/articles/how-to-native-iphone-ipad-apps-in-javascript/">iPod HTML5 javascript development</a>)<br />
<div><br />
</div><em><small>Simple things should be simple. Complex things should be possible.</small></em><br />
<br />
<br />
Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com1tag:blogger.com,1999:blog-2691799628167753841.post-86214239516989499312011-07-03T11:06:00.001-04:002011-08-11T09:04:50.711-04:00Simple jQuery contact applicationI continue to be impressed with how easy it is to build nice, useful applications with Seaside using the jQuery support. We have an old Unix character based contact application that those who don't want to change (and you know how you are) continue to use. I gave up on character based interfaces the late 80's, so I created a simple Seaside app to access the contact data.<br />
The contact data is dumped daily into a special character delimited file, with 40 fields per row. Trivial to parse in Smalltalk...<br />
<blockquote>array := aString subStrings: 1 asCharacter.</blockquote>...where aString is one row of the data file.<br />
<br />
Accessing the fields is just an array access, like...<br />
<br />
<blockquote>postalCode<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>^self dataArray at: 16</blockquote><br />
The collection of contacts (about 8500) is stored in a contact library object, in three collections, each sorted by a key stored on the object as a lower case string (by first name, by last name and by company). At first I tried using some fancy nested tree structure, then I tried large complex key dictionaries, but doing a match on 8500 instances takes less than 10 ms. Trying to optimize that just added complexity. Storing the search keys as lower case strings rather than translating them on each iteration was helpful. Search times dropped from 60 ms. The search method also has a limit to keep the auto-complete list is to a reasonable size. A '*' is appended to the string to support the general 'match' case.<br />
<br />
<blockquote><span class="Apple-tab-span" style="white-space: pre;"> </span>string := aString asLowercase.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>string last = $* ifFalse: [string := string copyWith: $*].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>list := aCollection select: [:each | string match: each nameKey].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>list size > limit ifTrue: [^list copyFrom: 1 to: limit].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>^list</blockquote><br />
We match on #nameKey, but the displayed list uses #displayFullName, the mixed case version of the string.<br />
<br />
OK, so the domain in as simple as it gets. The fun part was the Seaside code. JQAutocomplete>>search:labels:callback: made it easy (props to its author). The method wraps a lot of complexity into a tight, useful package. In my case, the code looks like this...<br />
<br />
<blockquote>html textInput<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>id: anId;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>style: 'width: 300px; font-size: 14pt';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>script: (html jQuery this autocomplete<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>search: [:string | self buildSearchResponse: aSearchBlock with: string]<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>labels: aLabelBlock<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>callback: [:value :script | self redirectToContact: value script: script])</blockquote>#buildSearchResponseh:with: is a hack to show error messages in the auto-complete list. An exception answers an error object which is a subclass of the contact class, which is then displayed in the drop-down list.<br />
<br />
<blockquote>^[aBlock value: aString]<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>on: Error<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>do: [:ex | Array with: (TSroloError newForErrorMessage: ex displayString)]</blockquote><br />
#aLabelBlock uses one of the three display methods for the drop down list (#displayFullName #displaySurname #displayCompany).<br />
<br />
#redirectToContact:script: shows a nice URL with a ?contact=nnnn suffix.<br />
<br />
<blockquote>| urlString |<br />
urlString := aScript requestContext request url startingURL ,<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>'?contact=',<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>aContact displayContactIndex.<br />
aScript goto: urlString.<br />
self session unregister</blockquote>In #renderContactOn: I check for the 'contact' parameter and render the contact details if found.<br />
<br />
Here is what it looks like...<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimsxorAQLB1U-5cS0bPCIHu6IM10PyIoY5tEq3k5DTidvlnkd75QGg1W0QJ5KS2l6WquRw0SbF5smNFb8-FEEYlYXa1thjlObFx9UKN9iV7J4bVHNEtQFcVzP8bPEM2JvqlAIuaaavs82M/s1600/1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="96" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimsxorAQLB1U-5cS0bPCIHu6IM10PyIoY5tEq3k5DTidvlnkd75QGg1W0QJ5KS2l6WquRw0SbF5smNFb8-FEEYlYXa1thjlObFx9UKN9iV7J4bVHNEtQFcVzP8bPEM2JvqlAIuaaavs82M/s400/1.png" width="400" /></a></div>...typing 'bob n' into the full name fields shows...<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiLHBW7kwVsV-udQx65YlFvyEZ3crnfMooagY52lYLPNLfm8U3-_xkIb8pbTTebm6G1qvpPangTZ21o25ucZaU9QmVpCM4-ZHdIST3LToOAtdKTcflib7tjz6ikMJDYRlL4L0inEROpdnd/s1600/2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="110" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiLHBW7kwVsV-udQx65YlFvyEZ3crnfMooagY52lYLPNLfm8U3-_xkIb8pbTTebm6G1qvpPangTZ21o25ucZaU9QmVpCM4-ZHdIST3LToOAtdKTcflib7tjz6ikMJDYRlL4L0inEROpdnd/s400/2.png" width="400" /></a></div>...selecting 'Bob Nemec' then redirects to '?contact=3784', which shows the contact details...<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgam6qvShw5QkFIw9QU7tzUcfsnnrtFm688urThW4HYxlX31eHVH8kjKEO4gd6a8W4ds7nFWxOk7j1BGgFfIWVGwec6MPAWWc_vVKz4TtV-q1Yiak100KSQKFciKHASZi6Vx7Bp3Gi9eHDP/s1600/3.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="127" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgam6qvShw5QkFIw9QU7tzUcfsnnrtFm688urThW4HYxlX31eHVH8kjKEO4gd6a8W4ds7nFWxOk7j1BGgFfIWVGwec6MPAWWc_vVKz4TtV-q1Yiak100KSQKFciKHASZi6Vx7Bp3Gi9eHDP/s400/3.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: left;">...the '+' toggles a display of all the contact fields, using the jQuery toggle...</div><div class="separator" style="clear: both; text-align: left;"></div><blockquote><span class="Apple-tab-span" style="white-space: pre;"> </span>| moreId lessId |<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>moreId := html nextId.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>lessId := html nextId.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html anchor<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>id: moreId;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>onClick: (<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html jQuery ajax script: [:s |<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: aComponentId) toggle: 0.3 seconds.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: moreId) hide.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: lessId) show]);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>title: aMoreTitle;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>with: [html image url: TSwaFileLibrary / #morePng].<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html anchor<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>id: lessId;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>style: 'display: none;';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>onClick: (<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>html jQuery ajax script: [:s |<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: aComponentId) toggle: 0.5 seconds.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: lessId) hide.<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>s << (html jQuery id: moreId) show]);<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>title: aLessTitle;<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>with: [html image url: TSwaFileLibrary / #lessPng]</blockquote><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">Finally, reloading the list of contacts is done by adding a ?load parameter, which loads the contacts from a predefined file location. This way the contact load action does not need to be done by the application, but can be done by an OS scheduled task.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">And again, a big thanks for the Seaside jQuery code which makes writing simple apps like this easy.</div><br />
<i>A bad day in [] is better than a good day in {}</i>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com3tag:blogger.com,1999:blog-2691799628167753841.post-61722200294271794072011-06-01T20:25:00.000-04:002011-06-01T20:25:43.395-04:00Low-tech Seaside graphsI really like the basic 'how to' tutorials people post, whether they are about Seaside, C#, VisualStudio or motorcycle maintenance (that pretty much covers where my head has been lately). <br />
<br />
So here are a couple of things I've done in Seaside that I think are simple yet proved handy.<br />
<br />
The first example is from a home financial application that I wrote a few years ago as a way to learn Seaside. My wife and I use it to track our budget and daily transactions, which it imports from a scheduled download done by an <a href="http://www.iopus.com/">iMacro </a>script.<br />
<br />
I wanted a bar graph to show how much of the budget for a category was spent for the month. And I wanted parts of the graph to support drilling down for more details. Here is what I ended up with (with some random data)...<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWkL7tnpa31N10WctOG4SXoGECjvI-_dBdP00kePUr8lbt7dZvPpkGya77H7FT56UtN75-iFM6eDU_9l-E-ud5ujchRkWbA9GZrLMI5npocuOtAa19YxNhZcYbcHI7Ev4CFaB3QSBh5sD5/s1600/Clipboard01.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="163" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWkL7tnpa31N10WctOG4SXoGECjvI-_dBdP00kePUr8lbt7dZvPpkGya77H7FT56UtN75-iFM6eDU_9l-E-ud5ujchRkWbA9GZrLMI5npocuOtAa19YxNhZcYbcHI7Ev4CFaB3QSBh5sD5/s400/Clipboard01.jpg" width="400" /></a></div><br />
...each square is one day of the month. The narrow grey line is to the right of today. If spending in a category is running ahead of the budget, it's red.<br />
<br />
The graph is created as a two column table, with the title and the progress bar. The bar is created by rendering same sized (16x16) image buttons, one for each day, and a narrow divider button (3x16) for 'today'.<br />
<br />
<blockquote>html imageButton<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>callback: [self openTransactions];<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>title: 'Show transactions';<br />
<span class="Apple-tab-span" style="white-space: pre;"> </span>url: TxFileLibrary / #progressredPng</blockquote><br />
<div class="separator" style="clear: both; text-align: left;">The images were created with <a href="http://www.getpaint.net/">Paint.net</a>, a handy tool for simple graphics.</div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">I used a similar approach to create a Kanban graph for an issue tracking system. In this case, a table with fixed sized cells is filled with image buttons representing an issue, to a maximum of five rows. Selecting a cell shows the list of issues for that cell, and the title of each cell shows the display string of the corresponding issue.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg17oRI-B_3F4cXxrx6_D_P14TaWU5NaUN43NPMH6aryZI87pB8geNjyJ4LfUpdovkGj3DV0Aoj_lEDqGsPV3qjuy3IKcN4AyFIhZM0D0oYbcTbh-Qhkc1dIWlR2kgaay9Ujornu58bkdn4/s1600/Clipboard02.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="300" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg17oRI-B_3F4cXxrx6_D_P14TaWU5NaUN43NPMH6aryZI87pB8geNjyJ4LfUpdovkGj3DV0Aoj_lEDqGsPV3qjuy3IKcN4AyFIhZM0D0oYbcTbh-Qhkc1dIWlR2kgaay9Ujornu58bkdn4/s320/Clipboard02.jpg" width="320" /></a></div><div class="separator" style="clear: both; text-align: left;"><br />
</div><div class="separator" style="clear: both; text-align: left;">I think it's cool what you can due with some dynamic HTML table generation and a few PNG files. </div>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-88261659828216245512011-05-20T08:08:00.000-04:002011-05-20T08:08:46.023-04:00User group marketingThe <a href="http://www.smalltalk.ca/">Toronto Smalltalk User Group</a> has been active for 20 years. At our peak we had about 45 people attending meetings. Lately we've averaged about a dozen.<br />
<br />
What can we do to get more people to attend?<br />
<br />
I suspect we already know all Smalltalkers in the Greater Toronto Area, if not personally then by just one level of separation. Our best bet is to target new people that may have an interest. We've had some success with Ryerson students. The two professors that sponsor our group encourage the students that take their OO course to attend. It has some Smalltalk content (Pharo) and a couple of students have come out.<br />
<div><br />
</div>Some of the TSUG crowd have attended <a href="http://www.xptoronto.com/">XP Toronto</a> events. They have regular meetings, pair programming pub nights and they organize well run Agile conferences. Those that know about Smalltalk respect it because of XP's roots, but they tend to be surprised that it's still an active language.<br />
<br />
Yanni Chiu has been something of a TSUG ambassador, representing Smalltalk at things like the "Dynamic Languages Smack Down". He and Chris Cunnington signed up for <a href="http://toronto.startupweekend.org/">Toronto Startup Weekend</a> and plan to show some Seaside app. I had some tentative plans to talk to the Durham Ruby group (something I need to follow up on).<br />
<br />
Whenever it's appropriate I hand out <a href="http://dl.dropbox.com/u/5602924/TSUG_Smalltalk.pub">TSUG pamphlets</a>. They list all the Smalltalk dialects, web frameworks and projects I know of. I print off a new batch for each TSUG meeting, with updated meeting schedules and conference information (ESUG in in the current one). It's an MS Publisher document which I print off on heavier stock paper which has a light blue tint. Here is what part of it looks like...<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuLNPKszY1V9J4q3aOba2XPul1v6hvtscOiuUA6l3bJSqTVn9LyMAWPA9aiIbg_bgLPz0Rs3mJBjxBAS04TV3IsLTRJeuFBBoSn_hkkS0i_V0QSDsUpyeDoxj7HFgF7f-UVDFmEvSnywsX/s1600/Clipboard02.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuLNPKszY1V9J4q3aOba2XPul1v6hvtscOiuUA6l3bJSqTVn9LyMAWPA9aiIbg_bgLPz0Rs3mJBjxBAS04TV3IsLTRJeuFBBoSn_hkkS0i_V0QSDsUpyeDoxj7HFgF7f-UVDFmEvSnywsX/s320/Clipboard02.jpg" width="286" /></a></div><br />
I'd love to get suggestions on how to promote the group.<br />
<br />
Malcolm Gladwell in his book <a href="http://en.wikipedia.org/wiki/Outliers_(book)">Outliers </a>makes the point that people with very high IQs are not more likely to be successful than people with just 'good' IQs (120, evidently). Once you're smart enough, other factors like people skills and communication skills make the difference. Smalltalk has an IQ above 180, the other tools are probably in the 120 range. Smalltalk needs to work on its people and communication skills.Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-86992264053683222882011-05-03T17:09:00.000-04:002011-05-03T17:09:30.632-04:00Pushing SmalltalkThe next meeting of the Toronto Smalltalk User Group is May 9 with Don MacQueen talking about JWARS.<br />
See the <a href="http://www.smalltalk.ca/">web site</a> for details.<br />
<br />
I've been involved with the Toronto Smalltalk User Group for 20 years now, the past dozen or so as the primary organizer. Lately we've had a few people show up that were new to Smalltalk and wanted to learn more. We talk to them about the simple syntax, show them Pharo and Seaside, tell them about the other dialects, and try whatever we can to get them enthused.<br />
<br />
But there is one core strength that I value which I cannot easily demo: the lack of brick walls.<br />
<br />
When working with tools like MS's Visual Studio, I'm constantly frustrated by the lack of universal object inspection and 'down to primitive' code tracking. Coding something new and with unfamiliar APIs gets a bit messy. You don't always know what you need until you trip over its absence. I find myself adding diagnostic code to show me stuff, which in Smalltalk I'd just debug and inspect. And sometimes in VS you just can't get what you want; you hit a brick wall. If it were not for Google and people posting esoteric workarounds, I don't know how I'd get anything done. And I love the examples that say "don't forget the comma, or else it won't work"... no error message, no warning, just no output.<br />
<br />
Thing is, that's not the kind of thing you appreciate until you try something complex, which does not happen to a new user. I wonder if we would benefit from having code examples with pre-defined bugs, written in various languages, and then using them to show how you'd debug the problem. Show just how few barriers we have in Smalltalk to understanding what is going on in our code (and, more importantly, in other people's code).<br />
<br />
I used to tell people that I saw a lot of similarity between programming in MVS 360 assembler and Smalltalk. For me, transparency was the big stick that I could use to whack the problem. From what I've seen of other IDEs (an admittedly short list), Smalltalk still does that the best.Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-42605860248515781472011-04-22T07:39:00.000-04:002011-04-22T07:39:20.730-04:00Smalltalk triviaI miss the days of The Smalltalk Report and discussions on comp.lang.smalltalk. It was a time of lively debate about syntax, naming conventions, coding patterns, dialects ... all the signs of an energetic community. It's not like that any more. I think it's due to the fragmentation of the Smalltalk forums. The threads are interesting, but specialised.<br />
<br />
My coding is strongly influenced by Kent Beck and his coding patterns. I remember reading his column in The Smalltalk Report and seeing the evolving consensus of how to code Smalltalk. My iteration block still have :each or :each<something> as the binding variable. In his "Fundamentals of Smalltalk Programming Technique", Andres Valloud makes the point that code formatting is a pointless debate and that we should be comfortable reading any Smalltalk code. And he's right. But I'd still like to know what others find more readable. It's like learning to write gooder English.<br />
<br />
For example, I trip over the extra white spaces inside blocks that most of the Seaside code has. Where I'd write a method like...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
aBoolean</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ifTrue: [self doThis]</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ifFalse: [self doThat].</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">aList do: [:each | self doSomethingWith: each]</span><br />
<br />
...I find the Seaside code is formatted as...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
aBoolean</span><br />
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ifTrue: [ self doThis ]</span></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ifFalse: [ self doThat ].</span></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">aList do: [ :each | self doSomethingWith: each ]</span></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
</span>...I wonder why that convention was adopted. In my eyes the block is just not hugging the code enough. I don't find that style in other Smalltalk code. And it really does not matter; I'd just like to understand the though behind it. </div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><br />
</div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">Another example: In our VW application, the framework code uses abbreviations for local and parameter variables, so I see a lot of code like <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">printOn: aStrm</span> and <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">| errMsg idx t v m s |</span> ... drives me nuts. But it was adopted because of how easy it used to be to name a local variable the same as an instance variable. We do refactor code this kind of code now as we maintain it, but it's easier to accept when the thought behind it is understood. </div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><br />
</div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">And here's a pattern question: when is it better to double-dispatch? In our VW code was have an #echo method which writes a printString of the receiver to the Transcript, like the newish VW #out method (unfortunate name conflict with an Opentalk method). We also have #echo: which writes both the receiver and the argument to the Transcript, so...</div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"> <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> 'Time' echo: Time now</span></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">...shows in the Transcript as...</div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> Time 10:10:54 AM</span></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><br />
</div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">I added the same code to our VA application. So what's the best way to write this method? We don't want strings to be printed with single quotes, like...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
Transcript cr; show: 'this string' printString </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">'this string'</span><br />
vs.<br />
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">Transcript cr; show: 'this string'</span></div><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">this string</span><br />
<br />
...so we can implement #echo on Object as <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">Transcript cr; show: self printString </span>and on String as <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">Transcript cr; show: self.</span> But what about #echo: ? We could implement on Object as...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
Object>>echo: anObject</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> Transcript cr; show: self printString, ': ', anObject printString</span><br />
<br />
...(I prefer the : delimiter to just a space) and we'd need...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
String>>echo: anObject</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> Transcript cr; show: self , ': ', anObject printString</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
</span><br />
...but what if anObject is a String? You would end up with superfluous single quotes. You could add a new method like #asEchoString with an Object and String implementation and send that instead of #printString, which is easy to read, or you could try double dispatching, which I decided to do (mostly out of curiosity). In this case I don't think it's a better pattern, but it's interesting (and very useful for more complex problems). And I like that they are all one line method.<br />
<br />
Here is what my implementation (no value returned so that <span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">a := b echo</span> answers b)...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">Object>>echo</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>self printString echo</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
String>>echo</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>Transcript cr; show: self.</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
Object>>echo: anObject</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>anObject echoAfter: self printString</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
String>>echo: anObject</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>anObject echoAfter: self</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
Object>>echoAfter: aString</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>aString echoString: self printString</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
String>>echoAfter: aString</span><br />
<br />
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>aString echoString: self</span></div><div><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><br />
String>>echoString: aString</span></div><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>Transcript cr; show: self , ': ', aString</span><br />
<br />
<div><br />
</div></div><div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;"><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">'test' echo: 123 >> 'test: 123'</span><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">'test' echo: 'this' >> 'test: this'</span><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">123 echo: 'this' >> '123: this'</span><span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">123 echo: 456 >> '123: 456' </span></div><div><br />
</div><br />
<div><br />
</div>Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com3tag:blogger.com,1999:blog-2691799628167753841.post-60691425576891566672011-04-19T10:14:00.000-04:002011-04-19T10:15:41.590-04:00VXML server isolationOur IVR VXML Seaside server needs to handle up to 700 calls per hour. Normally that's not a problem, but we do get a few timout errors per week, which is why we're moving to a three image load balanced deployment.<br />
<br />
Recently we had another issue with our current setup: the server hosting the file share, to which call files are written, failed. Calls were still processed, but the final write failed. Write are done in a forked block with an exception handler, so the image dealt with the errors cleanly. And the call data was all written to a log, so no data was lost.<br />
<br />
However, there was a performance hit on the server image and we started to get timeout errors on more than 10% of the calls. So we turned off the Seaside server which triggered an automatic failover to our AWS hosted server. It works, but it's a bit slow. Callers could wait several seconds for the call prompts (we'll be upgrading it soon).<br />
<br />
This was the first time we had an error with the file share and it pointed out a dependency that we'd prefer to avoid. Ideally, the VXML servers would continue to process calls even if other servers are down; they should as isolated as possible.<br />
<br />
Our strategy is to use the VW Opentalk image to image interface and have a data server gather call information from the three VXML servers. It will also send them updated validation files. This way, the VXML servers have no folder share dependencies, the data server can show status across all three VXML server, and the VXML servers can function even if the entire back office is down. And we can host the data & VXML servers with different service providers.<br />
<br />
We've been using Opentalk in our client patch system for a couple of years now. It's familiar and reliable. And ya gotta love the deployment flexibility you get with Smalltalk.Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0tag:blogger.com,1999:blog-2691799628167753841.post-70089128785786803472011-04-15T10:21:00.000-04:002011-04-15T10:21:34.084-04:00IVR VXML from SeasideWe use Seaside to serve anything that takes data from an http call. One of these services is an <a href="http://en.wikipedia.org/wiki/Interactive_voice_response">IVR</a> system hosted by <a href="http://www.voxeo.com/">Voxeo</a>. It requires a <a href="http://www.vxml.org/">VXML </a>file that defines the audio script and records answers. We have a sublcass of WARequestHandler that answers static VXML content for the initial request:<br />
<br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">handleRequest: aRequestContext</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| response | </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>response := aRequestContext response. </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>response </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>contentType: self contentType; </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>nextPutAll: <b>self document requestVXML</b>.</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>aRequestContext respond.</span><br />
<br />
<br />
...and builds contents for the response, which contains a simple 'Thank you' if all the data is valid...<br />
<br />
<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;">handleResponse: aRequestContext</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>| response result request | </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>request := aRequestContext request.</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span><b>result := self processCall: request.</b></span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>response := aRequestContext response. </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>response </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>contentType: self contentType; </span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>nextPutAll: result.</span><br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"><span class="Apple-tab-span" style="white-space: pre;"> </span>aRequestContext respond.</span><br />
<br />
<br />
Our production system, which our client uses to record at-home service calls, peaks at about 600 calls per hour. And the same image that serves the VXML file also has a conventional Seaside interface for monitoring the system and for configuration. Very handy. For example, we have a Google graph to show one day's call distribution.<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7M2jpnCHjgR8PccClPpIxHGot4AlOuPvKCSGFeI2VH1UEwzfEhIiL83IKp52yxZZ1NkOBECNvrg7oHkgmHl-EcUFzgArkQVnASyWEG1l5edwol2tCVeIGXh08gTV_LX8oyzAItA-8rqAY/s1600/2011-04-15+10-02-24+AM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="197" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7M2jpnCHjgR8PccClPpIxHGot4AlOuPvKCSGFeI2VH1UEwzfEhIiL83IKp52yxZZ1NkOBECNvrg7oHkgmHl-EcUFzgArkQVnASyWEG1l5edwol2tCVeIGXh08gTV_LX8oyzAItA-8rqAY/s400/2011-04-15+10-02-24+AM.png" width="400" /></a></div><br />
<br />
Recently we added more validation to the VXML script, so that shifts and employee numbers could be checked during the IVR dialog. The new interaction places more demands on the Seaside image, so we're testing a deployment with three images and simple round-robin load balancing (the current single image deployment gets about two timeout errors per week). Each VXML call is RESTful so we have no need for session affinity. Testing all went well, until something interesting came up.<br />
<br />
Testing was all done on a Windows server and deployment is on a Linux box. No big deal for VW. But when we tried the new deployment, the IVR system said the VXML content had errors. Looking at the VXML file on our Seaside display showed extra blank lines, but the content was correct. Inspecting the file content as read showed that the <cr><lf>s were all replaced with <cr><cr> (you gotta love all the delimiter differences between Windows, Linux and Smalltalk). I didn't think that IVR server would have an issue with that, but to be sure I replaced the file read from...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ^file contentsOfEntireFile</span><br />
...to...<br />
<span class="Apple-style-span" style="font-family: 'Courier New', Courier, monospace;"> ^file contentsOfEntireBinaryFile asString</span><br />
...and it turns out that fixed the problem.<br />
<br />
Makes we wonder what people do who work with tools that don't allow the deep diving that Smalltalk does. I suspect that when it all works their productivity is high, but there must be more frustration when things break.Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com1tag:blogger.com,1999:blog-2691799628167753841.post-32904001998909418802011-04-14T07:39:00.000-04:002011-04-14T07:39:49.411-04:00VA Smalltalk and SeasideI support a VA Smalltalk app for which we're adding a web interface, using Seaside. The first feature we've ported is a 'Site Map' view, a hierarchy of GIF images with configured 'hot point' rectangles for navigation. Users click to drill down to more other images or to display data in a table.<br />
<br />
In VA the display is updated by replacing the 'image area' #labelPixmap and adjusting the window shell width and height. 'Hotpoints' are managed by mapping 'Pointer Motion' and 'Button Press' callbacks to a collection of objects that define the rectangle, which shows an action specific mouse pointer, and and triggers the action when the mouse button is pressed. All of this is user configurable using a view where they add image, draw rectangles and define actions.<br />
<br />
A few years ago we added a feature which optionally displayed the current value of a measurement in the displayed image. It eliminated the need to click on a data 'hot point' and was handy for images with several data points. We've now added the same capability to the Seaside view.<br />
<br />
In VA, the data is displayed by first defining instances of CwLabel in the imageArea (a WkImageWidget) at the correct position. The #labelString and #backgroundColor are then set based on user parameters, like the display date. Background color is used to show the 'quality' of value, so a grey background indicates incomplete data. To hide the data, the CwLabel's #visible property is set to false.<br />
<br />
This is what it looks like in VA...<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkz-6PAS6tj4GP5ZjP_GXay4HHgQFj7BmBdBbHdDiAN4mG_xz-GTytuqT9c_bbGOaRuJfaHTLMjSZnIPM0aG4qQaknqmIcoTnzi2DDduXOnOyYjc8uC1s7Nz1-wCxbaZ3QiN9sKdB_et0K/s1600/Clipboard01.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="208" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkz-6PAS6tj4GP5ZjP_GXay4HHgQFj7BmBdBbHdDiAN4mG_xz-GTytuqT9c_bbGOaRuJfaHTLMjSZnIPM0aG4qQaknqmIcoTnzi2DDduXOnOyYjc8uC1s7Nz1-wCxbaZ3QiN9sKdB_et0K/s320/Clipboard01.jpg" width="320" /></a></div><br />
To do this in Seaside, we use a style 'position:absolute; left:10px; top:140px; z-index:-1;' for the GIF and then render the displayed data with...<br />
'position: absolute; top: %1px; left: %2px; z-index:2; border: 2px solid black; background-color: %3'<br />
...using #bindWith:with:with: to set the top, left and background-color. The result looks like this...<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNvNPg1yL_EOt7YuhG6wz3pcoTwUBvZOMXz7SUyTLIgn-ttVCfakddZt4myk6hvKr8L52tr7X7oaBJRSUhdHF-9AarZNWe689GY73ErGRXiYdi_TLoyyM7x8-BGwu6_GxNR0V_vWob87Z1/s1600/Clipboard02.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="206" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNvNPg1yL_EOt7YuhG6wz3pcoTwUBvZOMXz7SUyTLIgn-ttVCfakddZt4myk6hvKr8L52tr7X7oaBJRSUhdHF-9AarZNWe689GY73ErGRXiYdi_TLoyyM7x8-BGwu6_GxNR0V_vWob87Z1/s320/Clipboard02.jpg" width="320" /></a></div><br />
I continue to be impressed with how easy it is to do interesting web work in Seaside.Bob Nemechttp://www.blogger.com/profile/09778957926861701906noreply@blogger.com0