tag:blogger.com,1999:blog-368154312024-03-05T10:08:52.216-06:00Inching ForwardAs our circle of knowledge expands, so does the circumference of darkness surrounding it. -A.E.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.comBlogger32125tag:blogger.com,1999:blog-36815431.post-41165116090186667012015-02-07T12:44:00.000-06:002015-02-07T12:49:37.708-06:00Notes on Ecto MigrationsI got tripped up recently when trying to write my first <a href="https://github.com/elixir-lang/ecto">Ecto</a> migration. The style in Ecto changed from specifying migrations as DDL strings to a database-agnostic style via macros and functions. The problem I was running into is that most tutorials/posts refer to the older string-based style. I was using a newer version of Ecto with DDL strings and my tables weren't being created.<br />
<br />
The goal of this post is to give a basic overview of the new style and give some examples of its usage. All examples assume Postgres as the target database.<br />
<div>
<br />
In the new migration syntax, tables are created with the create macro and columns are specified using the add function. An example of an up migration:<br />
<br />
<pre class="brush: elixir">def up do
create table(:table_name) do
add :column1, :string
add :column2, :integer
...
end
end</pre>
<br />
The table's name is specified as an atom. Columns are defined by the add function: <br />
<br />
<pre class="brush: elixir">add(column, type \\ :string, opts \\ [])</pre>
</div>
<div>
<br />
where the first argument is the name of the column (an atom), the second argument is the type of the column (also an atom), and the remaining arguments are passed as options (more on this below). Ecto provides the following basic type atoms (the resulting Postgres column type follows as a comment):</div>
<div>
<br /></div>
<div>
<pre class="brush: elixir">:integer # integer
:float # double precision
:boolean # boolean
:string # character varying
:binary # bytea
:uuid # uuid
:decimal # numeric
:datetime # timestamp without time zone
:time # time without time zone
:date # date</pre>
</div>
<br />
Arrays are supported as composite-types having a tuple of :array and type atoms:<br />
<br />
<pre class="brush: elixir">{:array, :string} # character varying[]
{:array, :integer} # integer[]</pre>
<br />
The default type for the add function is :string, which results in an unbounded character varying type in Postgres. So this:<br />
<br />
<pre class="brush: elixir">add :post_body </pre>
<br />
is the same as this:
<br />
<br />
<pre class="brush: elixir">add :post_body, :string</pre>
<br />
The add function also accepts arbitrary atoms, allowing for database-specific types. Instead of using :string, :text could be used for Postgres's <a href="http://www.postgresql.org/docs/9.1/static/datatype-character.html">text</a> character type:<br />
<br />
<pre class="brush: elixir">add :post_body, :text # text instead of character varying</pre>
<br />
The add function's opts argument accepts the following options:<br />
<br />
<pre class="brush: elixir">:primary_key
:default
:null
:size
:precision
:scale</pre>
<br />
<div>
<b>Option Examples:</b></div>
<div>
<br />
By default, Ecto will create a serial primary key column for your table with a name of "id". You can override this behavior by passing "primary_key: false" in the table function and passing the "primary_key: true" option in a column's add function:<br />
<br />
<pre class="brush: elixir">create table(:table_name, primary_key: false)
add :doc_id, :uuid, primary_key: true
end</pre>
<br />
<b>Other options:</b><br />
<br />
Making a column not-null (the default is to allow nulls):<br />
<br />
<pre class="brush: elixir">add :title, :string, null: false</pre>
<br />
Giving a column a default value:<br />
<br />
<pre class="brush: elixir">add :author, :string, null: false, default: "Anonymous"</pre>
<br />
Limiting the size of a column:<br />
<br />
<pre class="brush: elixir">add :state_cd, :string, size: 2 # character varying(2)</pre>
<br />
Setting precision and scale:<br />
<br />
<pre class="brush: elixir">add :latitude, :decimal, precision: 14, scale: 11 # numeric(14, 11)</pre>
<br />
<b>Other:</b><br />
<br />
Ecto will add inserted_at and updated_at columns if you add a "timestamps" call in the body of the create macro:<br />
<br />
<pre class="brush: elixir">create table(:table_name)
add :title, :string
timestamps
end</pre>
<br />
Note that in Postgres, these are created as "timestamp without time zone" columns that do not allow nulls and do not have a default value. A default value can be specified in your model.<br />
<br />
There's a lot of stuff missing from this post, but it should be enough to get you up and running with the new migration style. Ecto is still young and changing. When in doubt, consult the <a href="https://github.com/elixir-lang/ecto">source</a>.<br />
<br /></div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-49077286547458018322014-11-03T19:02:00.001-06:002014-11-03T19:03:41.624-06:00Recommended F# Beginner ResourcesIt's still very early in my F# journey, but after spending some time with several resources available to newcomers, I have some recommendations. I believe the following list to be a decent path for moving from <a href="http://en.wikipedia.org/wiki/Dreyfus_model_of_skill_acquisition">Novice to Advanced Beginner</a> in F#. If you think I've missed some gems or disagree, please leave a comment.<br />
<br />
<br />
<a href="http://www.tryfsharp.org/">Try F#</a><br />
<br />
This site takes the "Try [insert language]" idea that has been done in other languages and really runs with it, providing several topic-oriented tutorials beyond the basics. It's a great way to get your brain prepped for F#. Note: I had some issues getting the script window to echo what I was typing in Safari on Mac OSX. I had to switch to Chrome in order to get it to work.<br />
<br />
<br />
<a href="http://web.archive.org/web/20110715231625/http://www.ctocorner.com/fsharp/book/default.aspx">The F# Survival Guide</a><br />
<br />
Even though it is dated, I consider this the best tutorial on F# for a beginner. The concepts are introduced in a logical progression that build on top of one another, the pacing feels right, the language used is simple and direct, and code is introduced in a way that can be run directly in fsi (a recurring theme in this post is learning by doing). After getting discouraged reading a couple of books where the authors failed to trim the context down in their examples or would bounce around concepts in a strange order, this resource gave me confidence. Once I finish this, I plan on going back to the books I gave up on because they are more current than the survival guide.<br />
<br />
For some reason, the F# Survival Guide's original website is no longer available, but the contents can still be found on the <a href="http://web.archive.org/web/20110715231625/http://www.ctocorner.com/fsharp/book/default.aspx">Internet Archive</a>. It's obvious the authors put a lot of effort into this excellent resource. It's a shame it doesn't have an official home or the ability to be updated.<br />
<br />
<br />
<a href="https://github.com/ChrisMarinos/FSharpKoans">F# Koans</a><br />
<br />
Many languages now have Koans, but the basic premise is the same: given a series of tests that cover the language's fundamentals, fill in blanks in the code that makes the tests pass. Starting from simple basics and moving to more complex concepts, the Koans are a great way to have simple problems put in front of you that require thinking in the language to solve.<br />
<br />
<br />
<a href="http://exercism.io/">Exercism.io</a><br />
<br />
I recommend not attempting these until you have some F# practice under your belt. Similar to the Koans, Exercism exercises present you with failing test cases that you must write code to fix. The difference from the Koans is that the exercises are not F#-specific and you can get feedback from mentors when you submit a solution. I wrote more about Exercism with F# in a <a href="http://inchingforward.blogspot.com/2014/10/using-exercismio-to-grok-f.html">post</a> that includes how to get set up using Xamarin Studio on Mac OSX.<br />
<br />
This one suffers a bit due to the size of the F# community participating. None of the F# exercises I've submitted have gotten feedback. Other languages have more mentors willing/able to provide feedback, and it does make a difference. Maybe a combination of attempting these and checking in with the F# irc channel would suffice?<br />
<br />
<br />
<a href="http://vimeo.com/97507575">Domain Modelling with the F# Type System</a><br />
<br />
This fantastic talk by Scott Wlaschin opened my eyes to algebraic types. Scott has a knack for taking complex concepts and making them accessible. Watching this talk made me feel like I've got a lot to learn (more fun ahead). It brings up some questions: <br />
<ul>
<li>What would business logic/service layers look like using these types? </li>
<li>How would these relationships be stored in an RDBMS?</li>
<li>What are the best practices for structuring large F# programs?</li>
</ul>
<br />
Which leads me to the following...<br />
<br />
<h3>
Continuing Education</h3>
<br />
Once you've read/watched the above, you should be comfortable with a lot of the F# that you will encounter. It's at this point where you enter a different phase: knowing enough to be dangerous but feeling ill-equipped to tackle more serious problems.<br />
<br />
For myself, I'm planning on building a couple of pet projects and writing about them, but I also need more directed learning. Something along the lines of <a href="http://fsharpforfunandprofit.com/series/a-recipe-for-a-functional-app.html">A Recipe for a Functional App</a> on <a href="http://fsharpforfunandprofit.com/">F# For Fun and Profit</a> (also by Scott Wlaschin, mentioned above). If you are reading this and have any recommendations, I'd appreciate them. Any books, talks? Are there any non-trivial F# code bases out there that exhibit best practices and are ideal for learners? Please share!<br />
<br />
If you are working on something targeted at teaching F# newbies and need a guinea pig or some feedback, let me know--I love learning this stuff and am interested in contributing.<br />
<br />mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-39977374635786372462014-10-28T22:40:00.001-05:002014-10-28T22:40:45.335-05:00Using Exercism.io to Grok F#Since I'm at the beginning of learning F#, I'm looking for different ways to get up to speed as quickly as possible and test what I've learned. To see how well I'm progressing, I've started doing the F# exercises on <a href="http://exercism.io/">Exercism.io</a>.<br />
<br />
<a href="http://exercism.io/">Exercism</a>, created by <a href="http://www.kytrinyx.com/">Katrina Owen</a>, provides learners a way to test their language comprehension by solving programming problems and getting feedback/mentoring by others. It's like a remote code review. After using it for a couple of languages and getting some insightful feedback, I've become a big fan.<br />
<br />
Here's how it works: sign up for an Exercism account, download the Exercism command line tool, fetch an exercise in the target language, solve the exercise, then submit it to the Exercism server. Once your solution has been published to the server, it is viewable by others who can mark it as "Looks Great", or give a "Nitpick". You are notified of any feedback. If the feedback suggests improvements, you can make a new revision and re-submit, or defend your choices and mark your solution as "Done." Once you have submitted a solution for a given exercise, you can also browse other member's solutions to that same exercise and leave feedback of your own. It's interesting to see how differently people solve the same problem, and to contrast other solutions with your own.<br />
<br />
An Exercism exercise consists of at least two files: (1) a Markdown-formatted README and (2) a test file. The README explains the exercise and the test file contains several tests written in the target language. You create a source file in the target language that makes the tests in the test file pass. Once the tests pass, you submit the solution, then fetch the next exercise and repeat.<br />
<br />
If you're at that point where you know enough F# to be dangerous but aren't sure where to go next, I highly recommend attempting the exercises on <a href="http://exercism.io/">Exercism.io</a>. The more people use it, the more value it will bring those seeking to learn. Feel free to log in and give me some Nitpicks--I can take it!<br />
<div>
<br /></div>
<div>
The remainder of this post walks through how I set my environment up (Mac OSX + Xamarin Studio) to do the Exercism problems. I added some tips and the end of the post that cover some problems I experienced.<br />
<br />
<br />
<h3>
Download Xamarin Studio</h3>
<div>
<br /></div>
<div>
I wrote about this in a <a href="http://inchingforward.blogspot.com/2014/10/getting-websharper-project-up-and.html">previous post</a>.</div>
<div>
<br /></div>
<div>
<br /></div>
<h3>
Sign up/install Exercism</h3>
<br />
Sign up for an <a href="http://exercism.io/">Exercism</a> account (requires a GitHub login) and follow the instructions to get started and download the command line tool. Make sure you have already run <i>exercism fetch fsharp</i> (to download the first exercise for just F#), or <i>exercism fetch</i> (to download the first exercise for all languages). The remaining steps assume you have the exercism command-line tool set up and have the <i>exercism</i> directory containing at least the first exercise ready to go.<br />
<div>
<br /></div>
<div>
<br /></div>
<h3>
Create a new F# Library Project in Xamarin Studio</h3>
<div>
<br /></div>
<div>
Run Xamarin Studio, then click the "New Solution..." button, choose "F# Library", and give your project a name:</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDP-EZMEKthOvouMyWR8lJaXU_wTR0-bpZJ6ULUpMDz_M8kTNl-XcyprMvuFZiENZSUGiK7KWHgEXfUJ7DjU0fqlHtpONywN4EOkPvC3MJpchjsHh98ROO6m032cIXsG-LCcAY6A/s1600/new_solution.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDP-EZMEKthOvouMyWR8lJaXU_wTR0-bpZJ6ULUpMDz_M8kTNl-XcyprMvuFZiENZSUGiK7KWHgEXfUJ7DjU0fqlHtpONywN4EOkPvC3MJpchjsHh98ROO6m032cIXsG-LCcAY6A/s1600/new_solution.png" height="193" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<div>
<br /></div>
<h3>
Install the NUnit and NUnit Runners packages</h3>
<div>
<br /></div>
<div>
Select the "Project" menu, then the "Add Packages..." menu item. When the "Add Package" window appears, type "NUnit" in the search field to limit the package results (there are a lot), then check the "NUnit" and "NUnit.Runners" checkboxes and click the "Add Package" button:</div>
<div>
<br /></div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnzaxgrCRDkHQTd05bmimMK02c2VdF7zgaHyR1Bo_8EHsptzl_3UfxJj1pDhYR9X2fzXFLJFGksYx3e8IbeiaH84-Vttov-AVelPJ11S1Mv7NM-sQxeTEDt0Pk8uwB8nU6Q-3Ryw/s1600/packages.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnzaxgrCRDkHQTd05bmimMK02c2VdF7zgaHyR1Bo_8EHsptzl_3UfxJj1pDhYR9X2fzXFLJFGksYx3e8IbeiaH84-Vttov-AVelPJ11S1Mv7NM-sQxeTEDt0Pk8uwB8nU6Q-3Ryw/s1600/packages.png" height="211" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<div>
<br /></div>
<h3>
Add the first exercise's test file</h3>
<div>
<br /></div>
<div>
When you ran the exercism <i>fetch</i> command, it should have created an <i>exercism</i> directory with an <i>fsharp</i> subdirectory. This is directory Exercism will fetch exercises to. Each exercise gets its own subdirectory. Inside the <i>fsharp</i> subdirectory, you should see a <i>sum-of-multiples </i>subdirectory. This is the first exercise.</div>
<div>
<br /></div>
<div>
Inside the <i>sum-of-multiples</i> subdirectory, you should see 2 files:</div>
<div>
<ol>
<li>README.md</li>
<li>SumOfMultiplesTest.fs</li>
</ol>
</div>
<div>
The README file is a Markdown-formatted text file that explains the exercise and gives some basic tips on how to solve it. The <i>SumOfMultiplesTest.fs</i> file is an F# source file that contains several <a href="http://www.nunit.org/">NUnit</a> tests that your solution file must pass before submitting. The test file should be added to the project.</div>
<div>
<br /></div>
<div>
Right-click on the project's name (not the solution's name), then choose "Add", then "Add Files...". In the "Add Files" window, navigate to the <i>sum-of-multiples</i> directory and select the <i>SumOfMultiplesTest.fs</i> file:</div>
<div>
<br /></div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkiNUb1Tm4VJ-CQj-cklIbvHWhdI_ncWkKjZpwJxMu9Gn6Fu9IUhWjpHo56SDdn4zDcKT4rAkdwDHmncAMzHVjV0D2Jz_C2Lnh17hn-WOTUxPtQhZfE1wT8ahMF1qNnfM_yyNFXA/s1600/add_files.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkiNUb1Tm4VJ-CQj-cklIbvHWhdI_ncWkKjZpwJxMu9Gn6Fu9IUhWjpHo56SDdn4zDcKT4rAkdwDHmncAMzHVjV0D2Jz_C2Lnh17hn-WOTUxPtQhZfE1wT8ahMF1qNnfM_yyNFXA/s1600/add_files.png" height="134" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<div>
<br /></div>
<h3>
Solve the exercise</h3>
<div>
<br /></div>
<div>
If you open the <i>SumOfMultiplesTest.fs</i> file in Xamarin Studio, you should see some errors:</div>
<div>
<br /></div>
<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/AVvXsEh0UvblHXGuHOvibwAKdR57HcWllmem9XruuP8uWLi4S3GI11isxdehXs8G02GXma2MwB-yn9YF3vhiRh1RhIiCw08udVPAg5a1DCXAiYEdUb2AbN1lbxtZQ5B5DFfHzfwg407BPQ/s1600/errors.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0UvblHXGuHOvibwAKdR57HcWllmem9XruuP8uWLi4S3GI11isxdehXs8G02GXma2MwB-yn9YF3vhiRh1RhIiCw08udVPAg5a1DCXAiYEdUb2AbN1lbxtZQ5B5DFfHzfwg407BPQ/s1600/errors.png" height="120" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<div>
Your goal for the exercise is to: (1) make the errors go away, and (2) make all the tests in <i>SumOfMultiplesTest.fs</i> pass. To do this, you'll need to create an F# source file that contains the missing functions and that fulfills the obligations of the tests. You can run the tests by selecting the "Run" menu, then the "Run Unit Tests" menu item.</div>
<div>
<br /></div>
<div>
Once all of the tests pass, copy your solution file back into the <i>exercism/fsharp/sum-of-multiples</i> directory, cd into that directory and type <i>exercism submit [solution filename]</i>. This will submit your solution to the Exercism.io server and allow it to be viewable by others. If you run the exercism <i>fetch</i> command again, it will pull down the next exercise. </div>
<div>
<br /></div>
<div>
<br /></div>
<div>
<h3>
Tips</h3>
<br />
The F# exercises require the answers to be written in the form of a module and a class containing the solution functions. Before attempting the problems, you should understand how F# classes work. <a href="http://fsharpforfunandprofit.com/posts/classes/">This post</a> on the awesome <a href="http://fsharpforfunandprofit.com/">F# for Fun and Profit</a> should help.<br />
<br />
The ordering of files in an F# project matters. Your solution file should come before the test file. You can re-order files by clicking on one, then dragging it above/below other files.<br />
<br />
The test files have the [<Ignore>] attribute set on all but the first test. Once you solve a test, remove the Ignore attribute from the next test and run the tests again or it will show up ignored in the Test Results window.<br />
<br />
<br />
<h3>
Good Luck! </h3>
<br />
<div>
<br /></div>
<div>
If you see anything wrong with the post or have comments, please let me know here or on <a href="http://twitter.com/inchingfwd">twitter</a>.</div>
<br />
<br /></div>
</div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-61111726798852411492014-10-19T13:26:00.001-05:002014-10-20T08:28:42.308-05:00Getting a WebSharper project up and running on Mac OS XA few weeks ago, <a href="https://news.ycombinator.com/item?id=8407468">this post</a> about Intellifactory's <a href="http://websharper.com/">WebSharper</a> showed up on Hacker News. The <a href="https://news.ycombinator.com/item?id=8409454">top comment</a> was enthusiastic enough for me to check it out. <br />
<br />
Since F# and Mono came out of the Microsoft ecosystem and I use Mac for development (and Linux for deployment), I had to do some digging to get things working. These are the steps I used to get a basic WebSharper app up and running on Mac OS X (10.9.x). Note that these steps are very high level and don't go into much detail.<br />
<br />
<h3>
1. Install Xamarin Studio</h3>
<div>
<br /></div>
I've used Xamarin Studio at work to create several cross-platform mobile apps. Being comfortable with that environment, I elected to go with Xamarin Studio for F#/WebSharper development. You can find the download <a href="http://xamarin.com/download">here</a>. Note that you will have to supply some personal info to proceed.<br />
<br />
Once the install begins, you will eventually get to this screen:<br />
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH38-9t_gQ6MTRigDX-JGTVDSKTVhpo06Y0o9F7Cd9_wQ5U4RIKNM5nuSrAddRVpOK66gREHYQXCHQr4ZqIfq_KJGrvlWcaP7b54nTpI5wJdHfMXWiwbSuhkBQgYrA1vVvpkp6eQ/s1600/xamarin-installer.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiH38-9t_gQ6MTRigDX-JGTVDSKTVhpo06Y0o9F7Cd9_wQ5U4RIKNM5nuSrAddRVpOK66gREHYQXCHQr4ZqIfq_KJGrvlWcaP7b54nTpI5wJdHfMXWiwbSuhkBQgYrA1vVvpkp6eQ/s1600/xamarin-installer.png" height="184" width="320" /></a></div>
<br />
<br />
In order for the install to work, you'll have to select at least one of the products in the list. For the purposes of doing WebSharper development, I don't think it matters which one(s) you choose.<br />
<br />
Once you get to the "Install Succeeded" screen, click the "Launch Xamarin Studio" button:<br />
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGqHneIhIo0TXe0j3P_B-j5aon4ryRjFk9lsXe_uHQRqmmjPEagjB4dFQ4QvRRSkmZi3WrheuvVG8OVPIDYCtYYfkqj-T4RpyyHvs0AwZKQ7JjLlsugWNoBYSjYkPpf7uUACD7xA/s1600/install-succeeded.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGqHneIhIo0TXe0j3P_B-j5aon4ryRjFk9lsXe_uHQRqmmjPEagjB4dFQ4QvRRSkmZi3WrheuvVG8OVPIDYCtYYfkqj-T4RpyyHvs0AwZKQ7JjLlsugWNoBYSjYkPpf7uUACD7xA/s1600/install-succeeded.png" height="131" width="320" /></a></div>
<br />
If you had already quit the install process, you can find Xamarin Studio in your Applications folder. When Xamarin Studio launches, it may have updates waiting. If so, click the "Restart and Install Updates" button.<br />
<br />
<h3>
2. Install Intellifactory's WebSharper Add-in</h3>
<br />
To make working with WebSharper in Xamarin Studio easier, Intellifactory provides the <a href="https://github.com/intellifactory/monodevelop.websharper">WebSharper add-in</a>. Installing the add-in will add project templates that make creating WebSharper applications easier. Click the "Xamarin Studio" menu, then choose "Add-in Manager...":<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiraRRnQjj2U3dZ70pnVzN0RgZEYRHNZBbkhmDquwV_P_jlU7eTLkScvCWSWLF8jgfPDPsjpKaXvmwphDU2Zg4svYx5Ht-kELq5QKMu0u46FsPg8R6WP4KH0nV6ilaLvbDa01-JJA/s1600/add-in-manager.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiraRRnQjj2U3dZ70pnVzN0RgZEYRHNZBbkhmDquwV_P_jlU7eTLkScvCWSWLF8jgfPDPsjpKaXvmwphDU2Zg4svYx5Ht-kELq5QKMu0u46FsPg8R6WP4KH0nV6ilaLvbDa01-JJA/s1600/add-in-manager.png" height="136" width="320" /></a></div>
<br />
<br />
With the Add-in Manager window open, click the "Gallery" tab, then click the "Repository" drop-down and choose "Manage Repositories...":<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxkkkFim74mrjxGQ2gxGrH7fY0MEy2j2_7unryhz6EblKszbY__5hw-ufwuQp4hdlFCevSEy8ewxx22HRMmvNBiy76zvBTCy8dmnbQww3owOX7xc_q3NBGPh9jMhKlZxJDnVxiDQ/s1600/manage-repositories.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxkkkFim74mrjxGQ2gxGrH7fY0MEy2j2_7unryhz6EblKszbY__5hw-ufwuQp4hdlFCevSEy8ewxx22HRMmvNBiy76zvBTCy8dmnbQww3owOX7xc_q3NBGPh9jMhKlZxJDnVxiDQ/s1600/manage-repositories.png" height="145" width="320" /></a></div>
<br />
<br />
From the "Add-in Repository Management" window, click the "Add" button. <br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihpAnY4IBv_fnaqQwjtrOzdaH0Zz5LYnJgPhwo2tF5GoJV70VkyfI4MTsjkBMqXhYEkRczbgf8zOE6VkicXpR8qeQjvjvqbC8sR4u5qM0MVG5UvbLIn5FMAYEmteolq9IFy5b3Qw/s1600/register-repository.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihpAnY4IBv_fnaqQwjtrOzdaH0Zz5LYnJgPhwo2tF5GoJV70VkyfI4MTsjkBMqXhYEkRczbgf8zOE6VkicXpR8qeQjvjvqbC8sR4u5qM0MVG5UvbLIn5FMAYEmteolq9IFy5b3Qw/s1600/register-repository.png" height="225" width="320" /></a></div>
<br />
<br />
Then paste the following URL into the "Register an on-line repository" URL field:<br />
<br />
<pre>https://raw.githubusercontent.com/intellifactory/monodevelop.websharper/master/repository/</pre>
<br />
<pre></pre>
<div>
...and click the "OK" button. Eventually, the add-in will finish installing. At this point, Xamarin Studio is ready for creating WebSharper applications.<br />
<br /></div>
<h3>
</h3>
<h3>
3. Create your first WebSharper Sitelet.</h3>
<div>
<br /></div>
<div>
There are several different kinds of WebSharper projects. We're going to create an app that has both a client and server piece, known as a "Sitelet". </div>
<div>
<br /></div>
<div>
Close all windows until you get back to the main Xamarin Studio screen. Then click the "New Solution..." button. In the "New Solution" window, find the WebSharper entry in the list in the left-hand pane (it's probably the last entry), then choose "WebSharper Sitelets Website". <br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxh0Kx1IVAfSPeTOyxrOuONev7eWolMczEjPM2Xh5ldaxvW8pTN3ItRMd_fT-lH8Imn2jr6f-0KdCDPVyFYnvKF_28GHF4pGtCQS614kOV9lpOYlcT5OIUBXYdwtxhPEqD48gtAQ/s1600/new-solution.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxh0Kx1IVAfSPeTOyxrOuONev7eWolMczEjPM2Xh5ldaxvW8pTN3ItRMd_fT-lH8Imn2jr6f-0KdCDPVyFYnvKF_28GHF4pGtCQS614kOV9lpOYlcT5OIUBXYdwtxhPEqD48gtAQ/s1600/new-solution.png" height="192" width="320" /></a></div>
<br />
<br />
Fill in a Name (I chose HelloWorld), make sure the project will be created in a directory you agree to, then click the "OK" button.<br />
<br /></div>
<h3>
</h3>
<h3>
4. Build the Project</h3>
<div>
<br /></div>
<div>
At this point, you now have a basic example starter project ready to explore. Let's see it in action. Before we can run it, we need to build it. Choose the "Build" menu item, then "Build HelloWorld". If everything went well, you should see a "Build successful." message.<br />
<br /></div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjxRJpRk5_xs6H2qYYmgdFWqk1Qcz5IQQCxtuyqn55z7RN9EyWW_VSXPecPVDIt8__pjVI04jv0Sr12qRmgEz7zHhdhE_SYgs5OsnvgLEiD75cJyxTeMNM2vjYF6nwmajVJp9IQw/s1600/build-successful.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjxRJpRk5_xs6H2qYYmgdFWqk1Qcz5IQQCxtuyqn55z7RN9EyWW_VSXPecPVDIt8__pjVI04jv0Sr12qRmgEz7zHhdhE_SYgs5OsnvgLEiD75cJyxTeMNM2vjYF6nwmajVJp9IQw/s1600/build-successful.png" height="56" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<h3>
</h3>
<h3>
5. Run</h3>
<div>
<br /></div>
<div>
From what I can tell, there's no way to run the application from inside Xamarin Studio yet. Open Terminal, then navigate to your project's directory. You need to be in the directory below the top-level similarly-named directory, in my case <i>HelloWorld/HelloWorld</i>:<br />
<br /></div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFsYKEER4Q47DNdUVSGGO-uqKw3hYY8V4bQIeiNUoiYGWt68IJCrsg-ZF6GK23eIR6EoTvSDrF-17Q6-OW6BuYvmv37-R6tJNnYc2MnJYeFkBwbW4n_zaCUYp3JCWzsz5RWEroRA/s1600/project-directory-terminal.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFsYKEER4Q47DNdUVSGGO-uqKw3hYY8V4bQIeiNUoiYGWt68IJCrsg-ZF6GK23eIR6EoTvSDrF-17Q6-OW6BuYvmv37-R6tJNnYc2MnJYeFkBwbW4n_zaCUYp3JCWzsz5RWEroRA/s1600/project-directory-terminal.png" height="254" width="320" /></a></div>
<div>
<br /></div>
<div>
<br /></div>
<div>
Once there, type the following on the command line:</div>
<div>
<br /></div>
<pre>xsp4</pre>
<div>
<br /></div>
<div>
This will start the xsp4 web server. You should see a "Listening on port: 8080..." message. Open a web browser to <a href="http://localhost:8080/">http://localhost:8080</a> and your app should display:<br />
<br /></div>
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD1tqrrqUtmnSguE5Q-435IKYig4U8O5-wTf0CBv1FXto_YBhg7SzegH4oy1jVZHH28lPvXWO97UiLRCwrOOT8MVJjf8R2P9rUzjdYaHGi0K1q6E4uis3aud8vRVWIyWa-XX_C-Q/s1600/web-app.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD1tqrrqUtmnSguE5Q-435IKYig4U8O5-wTf0CBv1FXto_YBhg7SzegH4oy1jVZHH28lPvXWO97UiLRCwrOOT8MVJjf8R2P9rUzjdYaHGi0K1q6E4uis3aud8vRVWIyWa-XX_C-Q/s1600/web-app.png" height="228" width="320" /></a></div>
<div>
<br />
<br />
Congratulations! If you got this far, you now have a development environment all set up to create the next amazing WebSharper application.<br />
<br /></div>
<h3>
</h3>
<h3>
Where to go from here</h3>
<div>
<br /></div>
This post just explained how to get a dev environment up and running. There is a BUNCH to learn. Try exploring the different pieces of the project, particularly the <i>Main.fs</i>, <i>Client.fs</i>, and <i>Remoting.fs</i> files. Keep in mind that if you make any changes, you will have to rebuild the project, and re-run the server.<br />
<br />
I am new to F# and WebSharper, but here's what I've been looking at:<br />
<br />
<ul>
<li>The <a href="http://fsharp.org/">F# Software Foundation</a> site</li>
<li>Scott Wlaschin's <a href="http://fsharpforfunandprofit.com/">F# for fun and profit</a></li>
<li><a href="http://www.nostarch.com/fsharp">The Book of F#</a> by Dave Fancher</li>
<li>The WebSharper <a href="http://websharper.com/docs">Learn</a> page</li>
</ul>
<br />
<h3>
</h3>
<h3>
Resources Used:</h3>
<div>
<br /></div>
Most of the information I used for this post was gleaned from the following:<br />
<br />
<ul>
<li>The F# Software Foundation's Use <a href="http://fsharp.org/use/mac/">F# on Mac OSX</a> page.</li>
<li><a href="http://websharper.com/blog-entry/3803">This</a> Intellifactory blog post.</li>
</ul>
<br />
If you liked this post, would like to see more posts on F#/WebSharper, or have corrections or criticisms, please leave a comment or let me know on <a href="http://twitter.com/inchingfwd">twitter</a>. Good luck exploring!<br />
<br />
<br />mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com2tag:blogger.com,1999:blog-36815431.post-4537134507664416062014-09-02T12:17:00.001-05:002014-09-02T12:17:56.412-05:00Eon: A Clojure Basics QuizAs part of an effort to avoid creating yet another blog engine, I started working on something completely different: a quiz/game called <a href="http://eon.mikejanger.net/">Eon</a>. <br />
<div>
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5y1Btl9uqfSvX3OB-uHkAPhG5BIzLrPfdyQ98CGG_EbyMS5ypNKRgllD_KjDUC3rIPYoqgy2R6h6UhFwNKxUraGH-gbowIK9kdP-50WwV80PReoFJy-SyHd2ZaB2W3byrwal-IQ/s1600/eon.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5y1Btl9uqfSvX3OB-uHkAPhG5BIzLrPfdyQ98CGG_EbyMS5ypNKRgllD_KjDUC3rIPYoqgy2R6h6UhFwNKxUraGH-gbowIK9kdP-50WwV80PReoFJy-SyHd2ZaB2W3byrwal-IQ/s1600/eon.png" height="260" width="400" /></a></div>
<div>
<br /></div>
<div>
Eon is a simple quiz designed to test basic Clojure evaluation comprehension. At the moment, it consists of 7 levels of 10 questions. Each question presents you with a Clojure form. You are given an answer box to submit your answer. An answer is initially worth 100 points. Entering an incorrect answer drops the possible point value by 10, so a perfect score at this point is 7000 (7 levels x 10 questions per level x 100 points).</div>
<div>
<div>
<br /></div>
<div>
I started writing Eon for a few reasons:</div>
<div>
<br />
<ul>
<li>I wanted to get out of a rut where every personal project started to <a href="http://inchingforward.blogspot.com/2013/12/breaking-out-of-blog-engine-cycle.html">look like a blog engine</a>.</li>
<li>I wanted to learn ClojureScript. There are <a href="https://github.com/jackschaedler/goya">lots</a> <a href="http://rigsomelight.com/2014/05/01/interactive-programming-flappy-bird-clojurescript.html">of</a> <a href="http://devartcodefactory.com/#/home">interesting</a> <a href="https://github.com/swannodette/om">things</a> <a href="http://holmsand.github.io/reagent/">happening</a> <a href="http://hoplon.io/">in</a> ClojureScript lately, and I wanted to get my feet wet. Eon is written in ClojureScript with <a href="https://github.com/holmsand/reagent">Reagent</a> handling the UI.</li>
<li>Working on this helped solidify my understanding of simple Clojure evaluation.</li>
</ul>
</div>
<div>
Eon is very basic, but I hope that other Clojure beginners out there find it useful. There are levels/questions that I'd like to add, but due to some limitations in the way I'm handling answers, I'm not sure I'll get to them. Plus, I'm already itching to create something more substantial using ClojureScript--it's a lot of fun to work with.</div>
<div>
<br /></div>
<div>
If you have questions, comments, or find bugs/see something wrong, please let me know via <a href="mailto:inchingforward@gmail.com?Subject=Eon">email</a> or on <a href="https://twitter.com/inchingfwd">Twitter</a>.
<br />
<br /></div>
</div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-52004030154330272672014-08-13T19:08:00.000-05:002014-08-13T19:08:20.521-05:002014 Clojure Cup IdeasClojure Cup 2014 is <a href="http://clojurecup.com/">just around the corner</a>. If you're excited about entering but don't have a clue what to make, maybe this post will help you. Below is a small collection of web app ideas I've had recently that I probably won't get to. I jot these things down whenever they pop into my head regardless of how ridiculous or difficult they seem. Most of them sit in a text file doing nothing. Maybe some of them can help someone directly or spark other ideas. I think the core functionality for most of these could be pulled off in a 48-hour period. Some may already exist, but attempting to build them might yield useful knowledge, or even win you the cup!<br />
<br />
<h3>
Storytellers</h3>
Connect with up to 3 others and collaboratively build a story by taking turns writing in units of paragraph, sentence, or word. Once the story is finished, publish it to a reading section of the site for others to read. <br />
<br />
<h3>
Version Check</h3>
Upload a project.clj file to this web service and you'll be told which libraries you're using that have newer versions.<br />
<br />
<h3>
Destination Helper</h3>
Specify a travel destination + dates of travel and get back details about the destination for that time period: weather, events, the most recent highly-rated restaurant reviews, etc.<br />
<br />
<h3>
Web-based Podcatcher </h3>
Log in, subscribe to feeds and listen without the need for a dedicated app. Is it possible to supplant native apps with a web offering?<br />
<div>
<br /></div>
<h3>
Due Today</h3>
Yes, this is another TODO list, but it's the only one I've been able to use consistently. You're only allowed to add items to today's date. If you don't complete an item, you have the option to move it to today from an earlier date. The focus is on entering actionable steps that can be achieved today.<br />
<br />
<h3>
Single Subject Search Engine</h3>
Indexing the entire internet is impossible for a sole developer with a VPS. Would it be possible to index a niche subject? What advantages would you have over a general search engine?<br />
<br />
<h3>
Source Code Recommender</h3>
You've learned the basics of a new language. You've solved some puzzles. You're still not comfortable writing in the language. Where do you go to see how advanced users of the language use it? With Source Code Recommender, reviewers can submit code repos that they think exemplify the best practice patterns and idioms of a language and rate/discuss/review/educate based on the source.<br />
<br />
<h3>
Goodreads Reviewer Suggester</h3>
Given your account, this site uses the Goodreads API to find other users that ranked books similarly to you. Receive suggestions for users to follow, books to read, or discussions to take part in.<br />
<br />
<h3>
Newsletter Aggregator</h3>
Email newsletters have taken off in popularity. This service aggregates all of your newsletters in one place. Sign up for an account and the service will supply you with an email address to use when signing up for newsletters. The aggregator then provides you with an interface for reading/managing/tagging/searching your newsletters.<br />
<br />
<h3>
Monument</h3>
Memories for locations. Search for a location on a map and add a memory (anonymously if you want) about that particular place/establishment/location. <a href="http://business.highbeam.com/435553/article-1G1-56374704/flaco-tacos-closes-all-three-restaurants">Flaco's Tacos</a>, I still miss you.<br />
<br />
<h3>
IT Interview Quiz</h3>
A site that quizzes you on those silly puzzles (and/or useful CS theory) that some companies use to filter the hiring pool. Submit answers to quizzes (anonymously if you want) and have them reviewed/rated by others and learn from other member's submissions. This is kind of an <a href="http://exercism.io/">Exercism.io</a> for theory.<br />
<br />
If one of these appeal to you, take it and run with it. Good luck and see you at Clojure Cup! <br />
<br />
P.S. If you use one of these and anything good comes out of it, you should buy me a burrito!<br />
<br />
P.P.S. If you're interested in making one of these outside of Clojure Cup and want a collaborator, let me know via <a href="mailto:inchingforward@gmail.com?Subject=Clojure%20Cup%20Ideas">email</a> or on <a href="https://twitter.com/inchingfwd">Twitter</a>.<br />
<br />mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com3tag:blogger.com,1999:blog-36815431.post-50114926518803558122014-05-14T21:36:00.000-05:002014-07-22T14:10:40.749-05:00Learning From Seymore Papert<div class="tr_bq">
From this <a href="http://www.media.mit.edu/video/view/spring14-2014-04-24-4">Learning from Seymore Papert</a> talk:</div>
<blockquote>
The children are not going to be able to invent Calculus. It took a genius and two hundred thousand years for that to happen. But in fact, another genius, Seymore [Papert] can figure out a way of contextualizing the important ideas of this so it fits perfectly into the child's world. That changed my life forever. Because once you see that done, it's kind of a duty thereafter to try and make it happen.
<br />
<br />
- Alan Kay </blockquote>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-31820720386988740722013-12-27T12:49:00.001-06:002014-07-22T14:03:48.140-05:00Breaking Out of the Blog Engine CycleLooking back at 2013, I can't help but notice that <a href="https://gatewayhackers.com/">every</a> <a href="http://clojurians.org/">web</a> <a href="http://bm.mikejanger.net/">app</a> I <a href="http://adventure.inchingforward.com/">made</a> is basically a blog engine. Each app has some type of Post model, possibly a Comment model, a view for a list of Posts, a view for the details of a Post, and a way to filter them based on users, tags, etc.<br />
<div>
<br /></div>
<div>
<div>
I hereby declare 2014 as the Year of Breaking Out of the Blog Engine Cycle.</div>
</div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-82296089413630726552013-12-15T20:45:00.001-06:002014-06-19T10:38:03.201-05:00Gateway HackersI made another thing: <a href="https://gatewayhackers.com/">Gateway Hackers</a>. It's a Hacker News-like link aggregation site with a focus on the the Greater St. Louis Area IT/design/startup audience.<br />
<br />
The feature set is minimal, but the site is functional. You can <a href="https://gatewayhackers.com/accounts/signup/">create a site account</a> or <a href="https://gatewayhackers.com/accounts/login/">log in using Github or Twitter</a>. Once you have an account you can submit links, comment, and create a profile (here's <a href="https://gatewayhackers.com/profiles/mj/">mine</a>).<br />
<br />
Links are displayed in reverse chronological order since the site isn't popular enough to warrant a scoring system. I try to post a link or two every day, with a focus on quality. I personally like links that teach, inspire, bring folks together, or promote the St. Louis IT community. The site has a <a href="https://gatewayhackers.com/about/#code-of-conduct">code of conduct</a>, modeled after the <a href="http://speakup.io/coc.html">Speak Up! Community Code of Conduct</a>.<br />
<div>
<br /></div>
There's a <a href="https://twitter.com/gatewayhackers">Twitter account</a> you can follow that tweets link submissions.<br />
<br />
<b>Motivation</b><br />
I wrote the site for a few reasons:<br />
<br />
<ul>
<li>I can't help myself…I have a web-app creation habit.</li>
<li>Things here are starting to heat up, and I wanted a place for St. Louis IT folks to share and discuss tech stuff.</li>
<li>I like the HN/Reddit UI for news.</li>
<li>The quality of Hacker News has diminished. </li>
</ul>
<br />
<br />
<b>Future Plans</b><br />
If the site gains any kind of popularity, I'd like to add the following:<br />
<br />
<ul>
<li><strike>A directory for St. Louis meetups and user groups.</strike> <a href="https://gatewayhackers.com/groups/meetups/">Done, updated twice daily.</a></li>
<li>A process that submits links a few days in advance of upcoming events.</li>
<li>"Ask Gateway Hackers" posts.</li>
<li>A monthly "Who's Hiring" post. </li>
</ul>
<div>
<br />
So, fellow Gateway Hackers--<a href="https://gatewayhackers.com/accounts/signup/">sign up</a>, <a href="https://gatewayhackers.com/accounts/login/">log in</a>, and participate!</div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-54368613553328521182013-09-29T19:26:00.000-05:002013-10-07T18:01:08.158-05:002013 Clojure Cup Postmortem<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN23TRWzoQQAmKyq_BtEKGzChJBH3Ve0dJcPOgC1GWPTM5_yjiD3PxDf3FYrEF49K9_jZrFlZFklwgxtQ3p1anGfzSJ1FcVyJbznr8nsauC4uAkgtXv5QRNLe873vrulcGqoVNjw/s1600/gearing-up-for-getting-down.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN23TRWzoQQAmKyq_BtEKGzChJBH3Ve0dJcPOgC1GWPTM5_yjiD3PxDf3FYrEF49K9_jZrFlZFklwgxtQ3p1anGfzSJ1FcVyJbznr8nsauC4uAkgtXv5QRNLe873vrulcGqoVNjw/s320/gearing-up-for-getting-down.jpg" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Gearing up for getting down.</td></tr>
</tbody></table>
<br />
I was exhausted after submitting <a href="http://clojurians.clojurecup.com/">Clojurians</a> as my entry for the <a href="http://clojurecup.com/">2013 Clojure Cup</a>. I figured that people who say "I could write that in a weekend" were full of crap, but now I know it for sure. Keeping up the energy level and sustained focus over a weekend of hard-core coding is tough stuff, even for a simple site like mine. There are so many little things that go into building even the simplest functional web app. Despite some sore eyes, it was a fantastic experience.<br />
<br />
Here are my thoughts on doing a 48 hour programming competition:<br />
<br />
<h3>
<b>Breaks</b></h3>
The importance of breaks cannot be overstated. Taking periodic breaks, moving around, and drinking fluids is a must. When I say break, I mean <i>away</i> <i>from the computer</i>. It is very easy to check the clock and realize you've spent the last 2 hours staring at the monitor typing away. It happened to me a couple of times. A timer might not have been a bad idea. Not taking breaks away from the computer made seemingly simple problems that cropped up harder to deal with, especially as it got later into the evenings.<br />
<br />
<h3>
<b>Support</b></h3>
If you have a family, having a supportive one is a must. I was an MIA husband and father for much of this weekend, although we went out on Saturday afternoon to eat some St. Louis pizza (I don't care what <a href="http://www.youtube.com/watch?v=pOTC_G6V6nM&feature=youtu.be&t=1m43s">Jimmy Kimmel thinks</a>). I'm lucky to have a family that allowed me to hole-up in the kitchen and let me work peacefully. They were great and I owe them something fun.<br />
<br />
<h3>
<b>Comraderie</b> </h3>
Engaging with the folks in the #clojurecup irc channel was worth it, especially for a team of one. I liked seeing how helpful everyone was towards one another. It was also fun watching the live feed on the Clojure Cup page. Seeing other Clojurian's setups was cool--people from all over the world were tweeting pics.<br />
<br />
<h3>
<b>48 hours is not a lot of time</b></h3>
Although I prepared on paper quite a bit for the competition, I had to cut features as time started running out. Time went from adequate to scarce very quickly. In this competition you had to set up a VPS from scratch in addition to writing your web app. I'm lucky that my setup and install went smoothly.<br />
<br />
<h3>
<b>Knowing the tools</b></h3>
I put off learning some tools (particularly ClojureScript and Pedestal) because I didn't think there would be enough time to learn them beforehand and be useful in them when it mattered. Not being 100% comfortable with Clojure and some of the libraries I used cost me some time, so this was a good choice. I struggled with a few things that would not have been a problem for experienced Clojure programmers. Having said that, I learned <i>a lot</i> in a very compressed amount of time.<br />
<br />
<h3>
<b>Tero Parviainen</b></h3>
I want to give a huge thanks to <a href="https://twitter.com/teropa">Tero Parviainen</a> and the Clojure Cup organizers. Tero put a great face on the competition and was quick to help out anyone with questions or problems. The amount of work required to pull something like this off must be huge. He handled it admirably.<br />
<br />
All in all, I had a blast and I'm proud to have been a part of Clojure Cup 2013. What a rewarding experience. Good luck to everyone who finished! See you next year?<br />
<br />
If you're a Clojure developer, go check out my app <a href="http://clojurians.org/">Clojurians</a> and register yourself! If you find it useful, <a href="http://clojurecup.com/app.html?app=clojurians">vote</a> for me when voting opens on Tuesday!<br />
<br />
<b>Update: </b>Clojurians now has a permanent home at <a href="http://clojurians.org/">http://clojurians.org</a>. The source is available at <a href="http://github.com/inchingforward/clojurians">GitHub</a>.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-60205798326673738702013-08-27T13:25:00.001-05:002013-08-27T13:25:37.499-05:00Adventures in LuminusA few weeks ago, I started to take a tour of some of the Clojure web frameworks. To get to know them better, I'm going to pick a few frameworks and build a simple database-backed "Choose Your Own Adventure"-like story engine . I like this idea because the project is easy to understand and has a simple architecture but is complex enough that I'll need to understand how each framework works. I also thought the end product might be fun.<br />
<br />
The first web framework on the tour is <a href="http://www.luminusweb.net/">Luminus</a>, a framework spearheaded by Dmitri Sotnikov (aka <a href="http://yogthos.net/">Yogthos</a>). My initial project, called Adventure, can be found <a href="http://adventure.inchingforward.com/">here</a>, with the source hosted on <a href="http://github.com/inchingforward/adventure">GitHub</a>. I also seeded the site with its first story titled "<a href="http://adventure.inchingforward.com/adventure/1">Cube Dweller</a>".<br />
<br />
Luminus refers to itself as a "micro framework". It is a <a href="http://leiningen.org/">Leiningen </a>template that ties several open-source Clojure libraries together and provides the starting scaffolding for a project's initial code base. Luminus sits on top of <a href="https://github.com/noir-clojure/lib-noir">lib-noir</a>, but other libraries can be removed or swapped out with whatever you prefer. Luminus embraces flexibility, suggesting ways of doing things, but allowing you to change just about anything.<br />
<br />
I found the scaffolding Luminus creates to be similar to what I had done in previous projects. The code layout is easy to understand, using common organization idioms. A starter project contains <a href="http://getbootstrap.com/">Bootstrap</a> for the initial look and feel, as well as example routes and templates for a Home and About page so you can see how it all works together. Luminius also has support for generating scaffolding for specific databases, Clojurescript support and a few other features using <a href="http://www.luminusweb.net/docs/profiles.md#profiles">profile hints</a>.<br />
<br />
Luminus also includes the recently-released <a href="https://github.com/yogthos/Selmer">Selmer</a> templating library (also by Sotnikov). If you are familiar with <a href="http://djangoproject.com/">Django</a>, you will be right at home with Selmer since it is modeled after Django's template syntax. Selmer includes many of <a href="https://docs.djangoproject.com/en/dev/topics/templates/">Django's tags and filters</a> and makes adding new ones easy. It also includes some of the more advanced features like template inheritance and dev-time reloading. Selmer was great for me--someone familiar with Django who wanted to ease into Clojure web development. It's also fast and decently documented. <br />
<br />
Although my project needs were modest, I found Luminus to be a big help. It let me quickly get in and move forward with my project rather than deal with starting-project ceremony. I feel like I have control of my project and can change whatever I want. I'm also interested in the support for introducing Clojurescript to Adventure. At the level I'm at with Clojure web development, I think starting with Luminus was a great decision.<br />
<br />
So, go forth and <a href="http://adventure.inchingforward.com/">Adventure</a> with <a href="http://www.luminusweb.net/">Luminus</a>!mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-21796585997122373552013-06-09T12:56:00.001-05:002013-08-12T13:34:23.246-05:00Adding Markdown support to Clabango templatesIf you're coming to Clojure web development from <a href="https://www.djangoproject.com/">Django</a>, you might be interested in <a href="https://github.com/danlarkin/clabango">Clabango </a>by Dan Larkin. Clabango is a Clojure templating library modeled after Django's. It supports several of the Django filters and tags (including the can't-live-without <i>include, block, if, for </i>and <i>extends</i>), but also allows you to extend them by writing your own.<br />
<br />
On my <a href="https://github.com/inchingforward/logbook">current side project</a>, I wanted to add support for writing text entries in markdown and have the templates output the text in html. Thanks to <a href="https://github.com/yogthos/markdown-clj">markdown-clj</a> by <a href="http://yogthos.net/">Dmitri Sotnikov</a>, this was very simple.<br />
<br />
In a <a href="https://github.com/inchingforward/logbook/blob/master/src/logbook/filters.clj">namespace</a> for my project's filters, I required markdown-clj, then passed Clabango's body argument to md-to-html-string:<br />
<br />
<pre class="brush: Clojure">(ns logbook.filters
(:require [clabango.filters :as filter]
[markdown.core :as md]))
(filter/deftemplatefilter "markdown" [node body args]
(when body (md/md-to-html-string body)))
</pre>
<br />
Note that the return value of the <i>deftemplatefilter </i>is a string, rather than the map that Clabango's current examples demonstrate. There is a <a href="https://github.com/danlarkin/clabango/issues/10">difference</a> between the source on Github and the binary on <a href="http://clojars.org/">Clojars</a>.<br />
<br />
Now wherever I need to convert text from markdown to html in a Clabango template, I pass the text to the new markdown filter:<br />
<pre class="brush: xml"><ul>
{% for entry in entries %}
<li>{{ entry.title|markdown }}</li>
{% endfor %}
</ul>
</pre>
I was really impressed by how simple and easy this was. Thanks guys!<br />
<br />
<b>Update</b>: Dmitri has released <a href="https://github.com/yogthos/Selmer">Selmer</a>.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com3tag:blogger.com,1999:blog-36815431.post-47419536370632659172013-04-21T11:59:00.002-05:002013-04-21T12:22:23.601-05:00(grok 'clojure)<blockquote class="tr_bq">
<b>Learn at least one new language every year.</b> Different languages solve the same problems in different ways. By learning several different approaches, you can help broaden your thinking and avoid getting stuck in a rut.<br />
- <a href="http://pragprog.com/the-pragmatic-programmer">The Pragmatic Programmer</a></blockquote>
<br />
Was that really written almost 15 years ago? Time flies.<br />
<br />
It's been a while since I learned a new language. I started messing around with <a href="http://clojure.org/">Clojure</a>, and it's been a blast. It is the most fun I've had programming since the first time I fired up Squeak and started poking around.<br />
<br />
I can't seem to break out of the "every project looks like a blog engine" mold, so I started another web app called <a href="http://logbook.mikejanger.net/">Logbook</a> (<a href="https://github.com/inchingforward/logbook">source</a>). Inspired by <a href="http://austinkleon.com/2010/01/31/logbook/">this post</a> by <a href="http://austinkleon.com/about/">Austin Kleon</a>, it'll be a very simple way of logging what you did for the day. I started keeping a personal logbook using text files a year ago, and really like it.<br />
<br />
Logbook uses Clojure, <a href="https://github.com/weavejester/compojure">Compojure</a>, and <a href="https://github.com/danlarkin/clabango">Clabango</a> (I gotta have my templates).<br />
<br />
<b>My Clojure Path</b><br />
<b><br /></b>
The following has helped me get to where I am so far:<br />
<br />
<ul>
<li>The <a href="http://www.infoq.com/presentations/Simple-Made-Easy">Simple Made Easy</a> talk by Rich Hickey. If you find yourself nodding along to this, Clojure may be something you're looking for.</li>
<li><a href="http://www.manning.com/rathore/">Clojure in Action</a> by <a href="http://s-expressions.com/">Amit Rathore</a>. It's a little dated (there's a <a href="http://www.manning.com/rathore2/">second edition</a> in the works), but this approachable book got me going.</li>
<li>The <a href="http://clojurekoans.com/">Clojure Koans</a> are a set of exercises designed to test your Clojure competence. The <a href="http://www.4clojure.com/">4 Clojure</a> exercises are similar, but track your progress.</li>
<li>The <a href="http://clojure-doc.org/articles/tutorials/basic_web_development.html">Basic Web Development</a> tutorial on Clojure Docs got me going with Ring+Compojure.</li>
<li>Tero Parviainen's <a href="https://gist.github.com/teropa/5346635">list of Clojure resources</a> has a lot of stuff to check out.</li>
<li>News can be found on the <a href="http://www.reddit.com/r/Clojure/">Clojure subreddit</a> and <a href="http://clojure.in/">Planet Clojure</a>. </li>
</ul>
<div>
So far I'm finding Clojure challenging but fun. It's a welcome punch in the brain if you feel you've been spinning your wheels.</div>
<div>
<br /></div>
<div>
<br /></div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-33067645385892031012013-02-21T23:55:00.001-06:002013-02-22T09:05:30.311-06:00Bookmarks migration<br />
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">
I migrated <a href="http://bm.mikejanger.net/">Bookmarks</a> off of <a href="http://www.heroku.com/">Heroku</a>. Heroku generously offers one free <a href="https://devcenter.heroku.com/articles/dynos">dyno</a> that you can use to try out their services. I found the Heroku environment, tooling, and documentation excellent. Unfortunately, when you are only using a single web dyno and it hasn't seen any traffic in one hour, the dyno is <a href="https://devcenter.heroku.com/articles/dynos#dyno-idling">idled out</a>. The next request requires restarting the dyno process, which takes seconds. I understand why they do it, and there's no way I'm going to complain about something that is free, but it is unworkable for Bookmarks. Since I'm the only one using my web app, it would idle frequently throughout the day. Trying to save a bookmark required a frustrating wait. Since I already have a VPS with <a href="http://linode.com/">Linode</a>, I migrated Bookmarks over there.</div>
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">
<br />
I have several web apps already running on Linode, and a few of them are now years old. Moving Bookmarks made me revisit the dated setup. I was running apache + mod_wsgi and nginx with all web apps using the global Python install. Many Django folks recommend using nginx + gunicorn or uwsgi these days. I chose gunicorn since it is popular and easy to set up. Each web app now runs in its own virtualenv and is managed using Supervisor. This setup makes it much easier to maintain and grow, and now Bookmarks is more responsive. </div>
<div style="margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px;">
<br /></div>
mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-5852461062960735782012-12-13T00:02:00.004-06:002012-12-13T00:05:04.933-06:00Bookmarks StatusMy horribly named bookmarking webapp "Bookmarks" is coming along. Reacquainting myself with Django has been fun and informative. The app currently has user support and basic saving of bookmarks, so I'm switching over to using it for my own bookmarks. The deployed version can be found <a href="http://bm.mikejanger.net/">here</a>, but signups are disabled so there's not much for others to see.<br />
<br />
For the deployment environment I decided to go with Heroku. The ease of set up, design, documentation and tooling won me over. I only had to make a few changes to get the app Heroku-ready: switching from a locally imported and uncommitted settings file (which never sat right with me anyway) to using environment variables, configuring static files to be served from another server, a simple database tweak, and going through the rest of their <a href="https://devcenter.heroku.com/articles/django">Getting Started with Django guide</a>. Once you have your project set up on Heroku, deploying changes is a simple git push. It's been one of the better experiences I've had setting up a project in a long time.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-45793501414369833222012-11-09T18:06:00.003-06:002012-11-09T19:54:44.340-06:00BookmarksIt's been a long time since I've written anything here. It's also been a while since I had any personal projects to write about. Things in the web and mobile worlds have changed quite a bit since my last entries.<br />
<br />
In order to get up to speed with modern Django and javascript web development, I decided to create a personal project and blog about it. The project is eponymously named "Bookmarks". It's an old idea (social bookmarking) built using modern web development tools/frameworks. I like Django, so that's my starting point. At first the app will be a simple CRUD html website using Django templates, then I'll add a REST API and pick some javascript framework for the front end. If I make it this far, I may add some goodies that make it more usable on a mobile device.<br />
<br />
The code for this project will always be available on <a href="https://github.com/inchingforward/bm">GitHub</a>. I haven't decided on how to host it yet (self-hosted, Heroku, Amazon, etc), so there's no working site yet.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-38243461510296105802009-03-15T11:34:00.001-05:002009-03-15T12:54:55.930-05:00Notes on getting started with HtDPI've been working my way through <a href="http://htdp.org/">How to Design Programs</a>. I'm about 1/3 of the way finished, and have some recommendations for people who attempt to study the book on their own:<br /><ul><li>If you are using the dead tree version of the book, be warned that it contains errata. <a href="http://htdp.org/2003-09-26/typos.html">This page</a> is required reading. The <a href="http://htdp.org/">online version</a> also contains hints that are not in the physical books.</li><li>There is a <a href="http://htdp.org/2003-09-26/Companion/">DrScheme companion</a> to the book that offers helpful tips.</li><li>Obviously, you should download the latest version of <a href="http://download.plt-scheme.org/">DrScheme</a> to do the exercises with. The book lets you know which language level you should be using by displaying an image like the following (this image is from the online version, which, when clicked, will display instructions on how to change the language level):<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPkirrqXxtcM5-Z6QlUCcjw7pmnOA0DHabkERKaVZ_guziETJyJ5NwCLxvRuqLqBaVCRc3vB7_RrkJVCYOQe8BewZqCZjL9Amq4ykmQVR-WuMKxPP06FgedXZOh1bbWXgqUNVwkA/s1600-h/language-level.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 107px; height: 112px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPkirrqXxtcM5-Z6QlUCcjw7pmnOA0DHabkERKaVZ_guziETJyJ5NwCLxvRuqLqBaVCRc3vB7_RrkJVCYOQe8BewZqCZjL9Amq4ykmQVR-WuMKxPP06FgedXZOh1bbWXgqUNVwkA/s400/language-level.png" alt="" id="BLOGGER_PHOTO_ID_5313473582159930514" border="0" /></a></li><li>The design recipes introduced in the book are not just something to glance at. They are the heart and soul of learning how to create solutions. If you are not creating data definitions, contracts, purposes, templates, examples, and tests, you are doing it wrong. Not following the design recipes may get you lost.</li><li>Collecting the design recipes and printing them out provides a useful reference.<br /></li><li>When writing tests, use <a href="http://docs.plt-scheme.org/htdp-langs/Test_Cases.html#%28form._%28%28lib._lang/htdp-beginner..ss%29._check-expect%29%29">check-expect</a>. The format is: (<span style="color: rgb(51, 51, 255);">check-expect</span> <span style="color: rgb(204, 0, 0);">expr1</span> <span style="color: rgb(204, 0, 0);">expr2</span>), where expr1 is the value of the function you are testing, and expr2 is what you expect the function to evaluate to. Your check-expect tests go in the definitions window. When you click the run button, a summary of your tests will be displayed to you:</li><li><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlHfprKyCECAWXpgK5lEDZk-Jswo5eruiKP7fIepqelOqv1pu8i0-hcaNgwu5aF9iu1mxTxGTJnRwc1aWfuOdCmztQcxTN-SG-ECq6DDJMb4yFshZaM_IHG539SCTPjijqQrCLTA/s1600-h/add-error.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 400px; height: 237px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlHfprKyCECAWXpgK5lEDZk-Jswo5eruiKP7fIepqelOqv1pu8i0-hcaNgwu5aF9iu1mxTxGTJnRwc1aWfuOdCmztQcxTN-SG-ECq6DDJMb4yFshZaM_IHG539SCTPjijqQrCLTA/s400/add-error.png" alt="" id="BLOGGER_PHOTO_ID_5313470443574188162" border="0" /></a> There are 2 mailing lists that can provide help with HtDP self study: the <a href="http://www.plt-scheme.org/maillist/">PLT Scheme</a> discussion mailing list, and the <a href="http://groups.google.com/group/study-htdp">Study-HTDP</a> google group. The PLT Scheme list contains Scheme experts (including the folks that wrote the book) and contributors to the environments, languages and modules making up PLT Scheme. The Study-HTDP list contains folks like you working through the book. I've found the PLT community to be incredibly smart, helpful, and friendly.<br /></li><li>For each section, I create a new file in DrScheme ("10.1.ss", "10.2.ss"<filename here="">, etc) using the File->Save Definitions As... menu item. If old definitions are required for the current exercise, the text is good about referring you back to the original exercise. This means opening up the old section file and copy/pasting those definitions into your current file.<br /></filename></li><li>Do not give up. If you are struggling with an exercise, take it one step at a time. You should be focusing on the data definition for the class of data you are dealing with, the design recipe for that class of data, your contract and purpose statements, and creating examples and tests. If these don't help, re-read the section. Have you missed something important that the section or previous sections detailed? If none of this turns on the light bulb, ask for help. I've taken the stance that the harder an exercise is to solve, the more important it is for me to solve it, both for being able to continue on with the rest of the book and for personal reasons. Some of the exercises are hard--challenge yourself!</li></ul>If you're reading this and have some tips to share, leave a comment. Good luck!mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-29662484800759523932008-02-21T22:00:00.000-06:002008-02-21T22:31:34.805-06:00To ReadIt's been a while since I've messed around with Squeak/Smalltalk.  The Squeak community has been busy!  After searching just a little bit,  I've found a lot of great catch-up reading:<br /><ul><li><a href="http://squeakbyexample.org/">Squeak by Example</a> - I haven't gotten far in this, but love it already<br /></li><li><a href="http://squeak.preeminent.org/tut2007/html/">Squeak Development Example</a> tutorial by Steven Wessels</li><li><a href="http://www.swa.hpi.uni-potsdam.de/seaside/tutorial">Seaside Tutorial</a> by the Hasso-Plattner-Institut</li><li><a href="http://onsmalltalk.com/">Ramon Leon's blog</a> is always on point</li><li><a href="http://tekkie.wordpress.com/">Mark Miller</a> has written some thoughtful stuff</li></ul><div><br /></div>mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-12508026105568673322007-12-22T00:36:00.000-06:002007-12-22T13:06:05.368-06:002007 personal study/projects reviewHere's a break down of how I spent my personal study/projects time in 2007<br /><br /><span class="Apple-style-span" style="font-size:large;">The good</span><br /># of new programming languages studied: 2<br />Studied programming languages grokked: Squeak Smalltalk<br />Studied programming languages mostly understood: Common Lisp<br /><br /><span class="Apple-style-span" style="font-size:large;">The bad</span><br /># of fiction books read: 3<br /># of personal projects completed: 0<br /># of personal projects started: 0<br /><br />The fact that I didn't start and finish a single project this year is really disappointing. The reason for this was lack of time. The time that I do have to study and work on projects occurs late at night, usually after the kids have gone to sleep. This year I spent all of it on learning the two languages.<br /><br />In retrospect, I am very happy to have focused on Smalltalk and Lisp. There is a lot to be learned from both, and the mind expansion you get from studying them is worth it even if you don't end up using them.   <br /><br />For 2008, I'd like to focus on projects.  There are several ideas that I have been thinking about for a while, and I'd like to get them out of my brain and functional.  Initially these will be done using Django, but who knows where the next year will take things.   <div> </div><div>Writing the Seaside tutorial was a great experience:  it forced me to focus on understanding the concepts and techniques behind the framework more than just doing recipe-style programming.  I highly recommend this approach to learning something new.  I plan to keep doing this, and would be interested in reading anything you create.<br /></div><div> </div><div><br /></div><div>For those who are still reading:  thanks for your positive comments, sorry I didn't write much in the second half of the year, and hope you didn't mind the focus shifts.  Here's to hoping there are some good posts in me for 2008. </div>mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com0tag:blogger.com,1999:blog-36815431.post-84425820594497604732007-04-09T22:00:00.000-05:002007-04-10T08:24:58.466-05:00Wanted: Migrating To a Newer Image Part 2In the last post we migrated the Wanted source code to a newer image by using a local Monticello repository. That gave us a working Wanted application, but the WantedItems that we created in the old image weren't copied over. This post will focus on copying the data out of the old image and into the new one.<span style="font-size:130%;"><br /><br />Write Out the Collection of WantedItems </span><br />Fire up the old Squeak image, open a workspace and execute the following:<br /><pre><br />| out |<br />out := DataStream newFileNamed:<br /> '/path-to-your-directory-here/wanted-items.dat'.<br />out nextPutAll: WantedDatabase wantedItems.<br />out close<br /></pre><br />This creates the temp variable <span style="font-weight: bold;">out</span> to hold a DataStream object pointing to a new "<span style="font-weight: bold;">wanted-items.dat</span>" file. The <span style="font-weight: bold;">nextPutAll</span> method takes a collection of objects and writes it to the receiving stream before closing it.<br /><br />Exit the old Squeak image--this will be the last time we need it. From now on, all things in these tutorials will be done in the new image.<br /><br /><span style="font-size:130%;">Read In the Collection of WantedItems</span><br />Fire up the shiny new image, open a new workspace, and execute the following:<br /><pre><br />|in|<br />in := DataStream fileNamed:<br /> '/path-to-your-directory-here/wanted-items.dat'.<br />[in atEnd] whileFalse: <br /> [WantedDatabase wantedItems add: in next].<br />in close<br /></pre><br />The above creates a new DataStream off of the "<span style="font-weight: bold;">wanted-items.dat</span>" file (notice we're using the <span style="font-weight: bold;">fileNamed</span> message to create the DataStream instead of <span style="font-weight: bold;">newFileNamed</span>). The <span style="font-weight: bold;">[in atEnd]</span> block will return false until all objects have been read from the file. We pass that block another block that adds each object read in (using the <span style="font-weight: bold;">next</span> message) to the WantedDatabase wantedItems collection. Then we close the DataStream.<br /><br />We now have our old WantedItems in the new image. Open up a browser and head to http://localhost:8080/seaside/wanted. You should see something like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqvbO88O80q18eq8hdPFDX6o-JNL5YyGcbWMZYbwQxgB5VjuuKVsg5x3Vng1C8_PhyphenhyphenDISQtnP1CJMSSrsf3CpTKbe_XhtG7VUXTSapAQyyNDEf_xI7CcaR805GXH6e0Z80Z0UcuQ/s1600-h/migrated-wanted-items.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqvbO88O80q18eq8hdPFDX6o-JNL5YyGcbWMZYbwQxgB5VjuuKVsg5x3Vng1C8_PhyphenhyphenDISQtnP1CJMSSrsf3CpTKbe_XhtG7VUXTSapAQyyNDEf_xI7CcaR805GXH6e0Z80Z0UcuQ/s400/migrated-wanted-items.jpg" alt="" id="BLOGGER_PHOTO_ID_5051644955182056306" border="0" /></a><br /><span style="font-size:130%;"><br />Enable File Serving in Commanche</span><br />Our new image is not serving our css since Commanche is not enabled to serve files. Paste the following into a new workspace and "do it":<br /><pre><br />| ma seaside |<br />HttpService allInstancesDo: <br /> [:each | each stop. each unregister].<br />WAKom stop.<br />seaside := WAKom default.<br />ma := ModuleAssembly core.<br />ma serverRoot: FileDirectory default fullName.<br />ma alias: '/seaside' to: <br /> [ma addPlug: [:request | seaside process: request]].<br />ma documentRoot: FileDirectory default fullName.<br />ma directoryIndex: 'index.html index.htm'.<br />ma serveFiles.<br />(HttpService startOn: 8080 named: 'httpd') plug: ma rootModule<br /></pre><br />That should get you to this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKMo36GkCN57BQWhSY5dg2yR2ktAsivCOODq2wThaTpNc3A6pM257lQudcnlRcr16yaBKPDPhO_BGz4nFpRweOrgBHCyNogELUDlIeRkYxTpFTPxIzZMjxoKw_yzJ4tH0rIphIkQ/s1600-h/migrated-wanted-items-css.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKMo36GkCN57BQWhSY5dg2yR2ktAsivCOODq2wThaTpNc3A6pM257lQudcnlRcr16yaBKPDPhO_BGz4nFpRweOrgBHCyNogELUDlIeRkYxTpFTPxIzZMjxoKw_yzJ4tH0rIphIkQ/s400/migrated-wanted-items-css.jpg" alt="" id="BLOGGER_PHOTO_ID_5051644955182056322" border="0" /></a><br /><span style="font-size:130%;">That Entered Date Format Has Been Bugging Me</span><br />Me too. We can solve that by adding a <span style="font-weight:bold;">formatBlock</span> to the WAReportColumn that handles the enteredDate in WantedList>>initialize:<br /><pre><br />add: <br /> ((WAReportColumn selector: #enteredDate title: 'Entered Date')<br /> formatBlock: <br /> [:enteredDate | enteredDate asDate mmddyyyy]; <br /> yourself);<br /></pre><br />We get the date to show up in mm/dd/yyyy format by sending the <span style="font-weight:bold;">asDate</span> message to the enteredDate of the WantedItem, then sending the <span style="font-weight:bold;">mmddyyyy</span> message to that date.<br /><br />While we're there, let's modify the "Can Buy" WAReportColumn to show 'Yes' or 'No' instead of the robot-like 'true' or 'false':<br /><pre><br />add: <br /> (WAReportColumn<br /> renderBlock: <br /> [:item | item isPurchasable <br /> ifTrue: ['Yes'] ifFalse: ['No']]<br /> title: 'Can Buy');<br /></pre><br />This is accomplished by sending the isPurchasable predicate the ifTrue:ifFalse: message with a "Yes" or "No" value respectively.<br /><br />Those changes should give you something like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjiO5QKeX7nbrWyKe8pYs9HCzNwkd-jDdsVutk1n-N_AsusMWhw3W6MGqOZ33iIgn_uVVlb9kmdXjho998C3yO6xzQK4TZj02J5nq-HJ65W8cuzhOucZUoIt7e9zXXLRzVb83dxeg/s1600-h/wanted-items-entered-date-and-can-buy-changes.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjiO5QKeX7nbrWyKe8pYs9HCzNwkd-jDdsVutk1n-N_AsusMWhw3W6MGqOZ33iIgn_uVVlb9kmdXjho998C3yO6xzQK4TZj02J5nq-HJ65W8cuzhOucZUoIt7e9zXXLRzVb83dxeg/s400/wanted-items-entered-date-and-can-buy-changes.jpg" alt="" id="BLOGGER_PHOTO_ID_5051644959477023634" border="0" /></a><br /><span style="font-size:130%;">Saving Changes</span><br />Since we made source changes, we should commit them to our local Monticello repository. Open a Monticello Browser and select the Wanted-Item package in the left pane, and the local repository in the right pane:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhPXh3Y8YwQmCoSlhaxbLpTzasHXHHXrRMa1EEORw-1MohjRzKuZT-wq0vJFc6XU9hAbQEjxXrpaxrBrp0JA102CcWh7It8X6GifX0cbrxC5jKTKsIUJffMHZyeMwlxQriEK9osYA/s1600-h/wanted-changes-monticello.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhPXh3Y8YwQmCoSlhaxbLpTzasHXHHXrRMa1EEORw-1MohjRzKuZT-wq0vJFc6XU9hAbQEjxXrpaxrBrp0JA102CcWh7It8X6GifX0cbrxC5jKTKsIUJffMHZyeMwlxQriEK9osYA/s400/wanted-changes-monticello.jpg" alt="" id="BLOGGER_PHOTO_ID_5051647252989559714" border="0" /></a>Notice that an asterisk appears before the package name: this signifies that changes have occured in the package. Click the Save button and enter a version message describing the changes we made:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfl2ABxmERGxyy3z41u0ytkVZK1MkRwILU2KOhausKSl27PrNZHX3XySyN5UVKewtSt1BT0PNlCYQ6HPtv_9M3YMeznATyaQykqXtxtZrl5b0uunURibqpZllE68tI5-sN-H-BPQ/s1600-h/wanted-item-monticello-changes-commit.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfl2ABxmERGxyy3z41u0ytkVZK1MkRwILU2KOhausKSl27PrNZHX3XySyN5UVKewtSt1BT0PNlCYQ6HPtv_9M3YMeznATyaQykqXtxtZrl5b0uunURibqpZllE68tI5-sN-H-BPQ/s400/wanted-item-monticello-changes-commit.jpg" alt="" id="BLOGGER_PHOTO_ID_5051649847149806530" border="0" /></a><br />When you hit Accept, a version info window will display letting you know that the changes were successfully saved. At this point, save the image so that the WantedItems will be persisted. And that's it for this post--we are now fully migrated over to the new image. By now it feels like we've kinda worn out the simplicity of saving the WantedItems in a collection in the image. If we had had the data in a store somewhere, we could have just pointed the new image there instead of this file reading/writing stuff. Future posts will address this.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com2tag:blogger.com,1999:blog-36815431.post-33405907685301892672007-04-01T09:09:00.000-05:002007-04-02T01:18:12.728-05:00Wanted: Migrating to a Newer ImageDamien Cassou recently <a href="http://lists.squeakfoundation.org/pipermail/seaside/2007-March/011390.html">announced</a> the release of a new Squeak-Web image which can be found <a href="http://damien.cassou.free.fr/squeak-web/squeak-web-95-2.zip">here</a>. This post will focus on migrating the Wanted application from the image we've been using to his new one. Before this post, I was keeping my squeak stuff spread out all over the place, and since I'm migrating to a newer image I thought I'd get a little more organized and detail my setup here.<br /><br /><span style="font-size:130%;">A More Disciplined Directory Structure</span><br />Instead of scattering everything all over the place, I created a proj directory with the following directory tree:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicxbbN0Tmvcr_T3WfvNYD9vizCEm_1QSDrhe44HQ0dW7HFMTgcJcNRMi4gg2hjEx4lZDfmE8IA3FlwuV8bbFFLsgSqWmgu4XZLwxa_tE8kg0SUE7xMArW2M01pCLdva2ndNSK-fQ/s1600-h/directory_layout.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicxbbN0Tmvcr_T3WfvNYD9vizCEm_1QSDrhe44HQ0dW7HFMTgcJcNRMi4gg2hjEx4lZDfmE8IA3FlwuV8bbFFLsgSqWmgu4XZLwxa_tE8kg0SUE7xMArW2M01pCLdva2ndNSK-fQ/s400/directory_layout.jpg" alt="" id="BLOGGER_PHOTO_ID_5048665261415462194" border="0" /></a><br /><ul><li><span style="font-weight: bold;">proj/squeak</span> is where all Squeak-related stuff goes</li><li>each directory in <span style="font-weight: bold;">apps</span> contains a combination of an image plus a vm</li><li><span style="font-weight: bold;">doc</span> is for all Squeak/Smalltalk related pdfs like <a href="http://www.iam.unibe.ch/%7Educasse/FreeBooks.html">free books</a></li><li><span style="font-weight: bold;">images</span> and <span style="font-weight: bold;">vms</span> contain different versions of squeak images and vms</li><li><span style="font-weight: bold;">repository</span> is for the local Monticello repository, which we'll get to in a bit</li><li><span style="font-weight: bold;">tmp</span> should be obvious</li></ul>The "apps/wanted" directory is a new directory created for the new squeak-web image. My old image that I've been using for the Wanted tutorial is sitting in a different location.<br /><br /><span style="font-size:130%;">Setting Up a Local Monticello Repository</span><br />We're going to use Monticello to handle the versioning of Wanted (there's some overview info on Monticello <a href="http://www.wiresong.ca/Monticello">here</a>). Fire up the image you've been using for the Wanted tutorial, then click on the World and select open... -> Monticello Browser. You should see this window:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzM7I5dEbiZSgIHJUgCsBueE5inhEktN63YxyUSpaXoRfuzqE8NLROmNQwRYn53F1i_Om7LeA-JRD7DAewUM7dU-bEcSi-mrsOcT6gdUvES6rYbMxk4Xc5kA4YDhGTsbBsJGnDMQ/s1600-h/monticello_browser.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzM7I5dEbiZSgIHJUgCsBueE5inhEktN63YxyUSpaXoRfuzqE8NLROmNQwRYn53F1i_Om7LeA-JRD7DAewUM7dU-bEcSi-mrsOcT6gdUvES6rYbMxk4Xc5kA4YDhGTsbBsJGnDMQ/s400/monticello_browser.jpg" alt="" id="BLOGGER_PHOTO_ID_5048673232874763586" border="0" /></a><br />Click the "<span style="font-weight: bold;">+Package</span>" button. Enter "Wanted-Tutorial" (or the name of the package containing the Wanted tutorial objects in your image) in the "Name of package" dialog:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpgSbu4H25jHK_g4ImwcoIJOE_daiwu7R062fh8uJdsq4SvwsWFFsftElcnOUKXP2hphoGF8tUyXLvvLiERa3MqCIEd3-izyvEDMQHfZ27OPXuuQyM1TIyzsBtUwgMEla-MvH0fw/s1600-h/name_of_package.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpgSbu4H25jHK_g4ImwcoIJOE_daiwu7R062fh8uJdsq4SvwsWFFsftElcnOUKXP2hphoGF8tUyXLvvLiERa3MqCIEd3-izyvEDMQHfZ27OPXuuQyM1TIyzsBtUwgMEla-MvH0fw/s400/name_of_package.jpg" alt="" id="BLOGGER_PHOTO_ID_5048674306616587602" border="0" /></a><br />and click the Accept button. Then add a local repository by clicking the "<span style="font-weight: bold;">+Repository</span>" button, and choosing "<span style="font-weight: bold;">directory</span>" from the "Repository type" dialog:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5DkSaVkdiO1nyHHQBsho_q6-XFTbeL3Bm_-LefASsVZ7DfOUiv1DFwfy5H6QM0_T4aOpDD1VdfWvfhPQ90xWAzl7mwE2Ot55XUgW5K-tIFBWLHkDkRDX4Y2VLDcKfxui9znT-VQ/s1600-h/repository_type.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5DkSaVkdiO1nyHHQBsho_q6-XFTbeL3Bm_-LefASsVZ7DfOUiv1DFwfy5H6QM0_T4aOpDD1VdfWvfhPQ90xWAzl7mwE2Ot55XUgW5K-tIFBWLHkDkRDX4Y2VLDcKfxui9znT-VQ/s400/repository_type.jpg" alt="" id="BLOGGER_PHOTO_ID_5048676192107230562" border="0" /></a><br />The "Repository folder" dialog will then display:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxeIBHr-sXsdAA6xSIDsS6tUPNDujd0Ze0eD7iOK5Wh31NuZuXcbQhBEVWsBoI5KlbIrUtNLt0ghPsWlkxm3llGvv_2kTPVXa6mA-ys2lDRpyJ01WwyfPRSd5iyFK_rnifIOIPag/s1600-h/repository_folder.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxeIBHr-sXsdAA6xSIDsS6tUPNDujd0Ze0eD7iOK5Wh31NuZuXcbQhBEVWsBoI5KlbIrUtNLt0ghPsWlkxm3llGvv_2kTPVXa6mA-ys2lDRpyJ01WwyfPRSd5iyFK_rnifIOIPag/s400/repository_folder.jpg" alt="" id="BLOGGER_PHOTO_ID_5048676917956703602" border="0" /></a><br />Navigate to the folder you want to store the local Monticello repository in and hit the "ok" button. You should now be looking at the Monticello browser with the Wanted-Tutorial package selected in the left pane and the repository folder selected in the right pane. Click the "<span style="font-weight: bold;">Save</span>" button in the row of buttons in the Monticello browser. Enter you initials in the dialog that pops up and Accept. The "Edit Version Name and Message" dialog will pop up:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrq9PMR4ESbkYq2OqawI6Qe2fA2HKcnN2w9JBiOhD-JuuR7j2RDfBUJ8A9C8n-xgUzodxt4o4RkTA0vjaB6kcOvEnw8C2JOwVmCjxMeLrxd8o7w6VT29uqEQ2U1NC3aiHjWrsgDw/s1600-h/edit_version_name_and_message.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrq9PMR4ESbkYq2OqawI6Qe2fA2HKcnN2w9JBiOhD-JuuR7j2RDfBUJ8A9C8n-xgUzodxt4o4RkTA0vjaB6kcOvEnw8C2JOwVmCjxMeLrxd8o7w6VT29uqEQ2U1NC3aiHjWrsgDw/s400/edit_version_name_and_message.jpg" alt="" id="BLOGGER_PHOTO_ID_5048690137866040706" border="0" /></a><br />Enter a comment and Accept. If all went well you should end up with a window like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjglSHRHFSWRTgjztD3UTBiMGH-ns5jWE9h_GCBVBQTvS5_ZLeYf0paDje_vx9PmQFmZsWBg7gEi7r6dBMdLzRhooToYCy2uy6hmXfZnuTfKPtP-ncIuuZXH2NblRRGtIa1ZnLctw/s1600-h/version.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjglSHRHFSWRTgjztD3UTBiMGH-ns5jWE9h_GCBVBQTvS5_ZLeYf0paDje_vx9PmQFmZsWBg7gEi7r6dBMdLzRhooToYCy2uy6hmXfZnuTfKPtP-ncIuuZXH2NblRRGtIa1ZnLctw/s400/version.jpg" alt="" id="BLOGGER_PHOTO_ID_5048691439241131410" border="0" /></a><br />We now have a working versioning repository for our Wanted tutorial code. As you'll see, migrating it to newer images will be a snap. At this point, close the old image but keep it around--we'll need it later.<br /><br /><span style="font-size:130%;">Setting Up the New Image<br /></span>After downloading Damien's <a href="http://damien.cassou.free.fr/squeak-web/squeak-web-95-2.zip">new image</a> and unpacking it into the "images/squeak-web-95-2" directory (see the directory layout above), I copied the<br /><ol><li>squeak-web-95-2.image</li><li>squeak-web-95-2.changes </li></ol>files into the "apps/wanted" directory. I then copied the<br /><ol><li>Squeak3.8.xxx</li><li>SqueakV39.sources </li></ol>files from the current vm in the "vms" directory into the "apps/wanted" directory. I also copied the<br /><ol><li>wanted.css </li></ol>file from the old image directory into "apps/wanted". Once all 5 files are copied, in "apps/wanted", drag the squeak-web-95-2.image file over the Squeak executable to open Squeak with the new image. Collapse the Script Manager and Preference Browser windows to make some room.<br /><br />Open a Monticello Browser and click the "<span style="font-weight: bold;">+Repository</span>" button. Choose "directory" from the "Repository type" and select the directory you created above. With the directory repository highlighted in the right pane in the Monticello Browser, click the "<span style="font-weight: bold;">Open</span>" button. You should see a window like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP3-oiuGHi4YlujZVC75Zou29ZVwLws5TCBR7QxwlzJwwlkR_o7oLYOOmyvZn4IgEFnZsXUAW2aYMq50YWRXbuEfpSLkytgp1ZQbzTgBRQ2WcxBTmsP1HibL5207GEmIdqPwsMbQ/s1600-h/wanted_repository.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP3-oiuGHi4YlujZVC75Zou29ZVwLws5TCBR7QxwlzJwwlkR_o7oLYOOmyvZn4IgEFnZsXUAW2aYMq50YWRXbuEfpSLkytgp1ZQbzTgBRQ2WcxBTmsP1HibL5207GEmIdqPwsMbQ/s400/wanted_repository.jpg" alt="" id="BLOGGER_PHOTO_ID_5048698641901286818" border="0" /></a>Highlight the "Wanted-Tutorial...1.mcz" file in the right pane (the version information will appear in the bottom pane when you do so), then click the "<span style="font-weight: bold;">Load</span>" button. After the progress dialogs go away, open a new Refactoring Browser and scroll to the bottom of the Packages list:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbpZEZOuVqvUzzSZLoCqvnkcxorGRKa2FXpfQI6UdwDOHaGDPlV3b-gWMaJt34jLaUAkK46QgmfpvVwKfy_DJT670p1ZWdCGYKPRsy6d8fRvFsVU8nCMz4kjwvczDfkSgqtQLsmw/s1600-h/wanted_from_repo_rb.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbpZEZOuVqvUzzSZLoCqvnkcxorGRKa2FXpfQI6UdwDOHaGDPlV3b-gWMaJt34jLaUAkK46QgmfpvVwKfy_DJT670p1ZWdCGYKPRsy6d8fRvFsVU8nCMz4kjwvczDfkSgqtQLsmw/s400/wanted_from_repo_rb.jpg" alt="" id="BLOGGER_PHOTO_ID_5048700394247943602" border="0" /></a>Well looky there! I'll wait for you if you want to get up and do a happy dance.<br /><br />In this new image, Seaside doesn't know about our Wanted app yet because we haven't registered it. If you open a web browser to http://localhost:8080/seaside/wanted you will get a listing of the registered Seaside apps, and Wanted isn't one of them. Let's fix that. Open a Shout Workspace, enter the following, highlight and "do it":<br /><br /><code><br />WantedList registerAsApplication: 'wanted'<br /></code><br /><br />Now go to http://localhost:8080/seaside/wanted. Notice that our app shows up, but all of our WantedItems are missing! Stay tuned--we'll fix that in the next post. For now, save the new image. I'm just going to use the default "squeak-web-95-2.image" name instead of naming it "wanted.image" since it's already in the "wanted" directory. If you wanted you could name it "why-is-sanjaya-not-voted-off-yet.image" and the world would still turn and make bad decisions and watch bad TV.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com2tag:blogger.com,1999:blog-36815431.post-40536247946267716562007-02-21T04:06:00.000-06:002007-02-21T02:08:55.196-06:00Wanted: Beautification is EasyThis post will focus on adding style to Wanted with some very simple changes.<br /><br /><span style="font-size:130%;">Step 1: Enable File Serving in Commanche</span><br />If you've been following the Wanted tutorials and are using the squeak-web image, you'll have to restart the Commanche server and enable file serving. Enter the following in a new workspace, highlight it, and "do it":<br /><pre><br />| ma seaside |<br />WAKomEncoded39 stop.<br />seaside := WAKomEncoded39 default.<br />ma := ModuleAssembly core.<br />ma serverRoot: FileDirectory default fullName.<br />ma alias: '/seaside' to: [ma addPlug: [:request | seaside process: request]].<br />ma documentRoot: FileDirectory default fullName.<br />ma directoryIndex: 'index.html index.htm'.<br />ma serveFiles.<br />(HttpService startOn: 8080 named: 'httpd') plug: ma rootModule<br /></pre>See David Shaffer's Seaside <a href="http://www.shaffer-consulting.com/david/Seaside/GettingSoftware/index.html">tutorial</a> for more details.<br /><br />This will stop Commanche and restart it with support for serving files and Seaside applications. Note that it sets the document root to <span style="font-weight: bold;">FileDirectory default</span>. If you are starting Squeak by dragging the image file onto the Squeak executable, this should be the directory of the Squeak executable. You can find out exactly what it is by entering "<span style="font-weight: bold;">FileDirectory default fullName</span>" in a workspace, highlighting it, then doing a "Print It".<br /><br />Test it by flipping to your browser and going to http://localhost:8080. You should see a directory listing of the default directory.<br /><br /><span style="font-size:130%;">Step 2: Create a CSS File</span><br />Create a new text file called "<span style="font-weight: bold;">wanted.css</span>" in the default directory that contains the following:<br /><pre><br />body {<br /> margin:50px 0px; padding:0px;<br /> text-align:center;<br /> font: 12px arial, sans;<br />}<br /><br />table {<br /> margin-left: auto;<br /> margin-right: auto;<br /> border: 1px solid #848370;<br /> border-collapse: collapse;<br /> margin-bottom: 20px;<br />}<br /><br />th {<br /> background-color: #b4bfcc;<br /> padding: 4px;<br /> font-size: 14px;<br />}<br /><br />td {<br /> padding: 4px;<br />}<br /><br />a {<br /> color: #103b61;<br /> text-decoration: none;<br />}<br /><br />a:hover {<br /> text-decoration: underline;<br />}<br /><br />form {<br /> margin-left: auto;<br /> margin-right: auto;<br /> width: 400px;<br /> text-align: left;<br /> background-color: #dee3e4;<br /> border: 1px #ccc solid;<br /> padding: 10px;<br />}<br /><br /><br />form input, textarea {<br /> border: 1px solid #b4bfcc;<br /> padding: 2px;<br /> width: 98%;<br /> margin-bottom: 10px;<br />}<br /><br /><br />form textarea {<br /> height: 150px;<br /> font: 12px arial, sans;<br />}<br /><br />form input.submit {<br /> margin: 0;<br /> width: 50%;<br />}<br /></pre><br /><span style="font-size:130%;">Step 3: Wire the CSS File Up to the Components</span><br />To link to the css file, we'll implement updateRoot in WTComponent:<span style="font-weight: bold;"></span><br /><pre><br />updateRoot: anHtmlRoot<br /> super updateRoot: anHtmlRoot.<br /> anHtmlRoot linkToStyle: '/wanted.css'<br /></pre>Since both of our view components (WantedList and WantedEditor) inherit from WTComponent, they will both contain the link to the css file.<br /><br />Flip back to the browser and refresh:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg39j9F6ZReLNviphyphenhyphenzZnV6n9A5G5DpNFAaQx2wTI9NLPsbnYlQb82QyxUISPCydg-eoabYanxQFYNt6eokOKNpEFrfOgoPbRKKtGP-v-Wpdc-vp7LM7GPkA2X42otokoxkqBGeHw/s1600-h/styled_todo_list.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg39j9F6ZReLNviphyphenhyphenzZnV6n9A5G5DpNFAaQx2wTI9NLPsbnYlQb82QyxUISPCydg-eoabYanxQFYNt6eokOKNpEFrfOgoPbRKKtGP-v-Wpdc-vp7LM7GPkA2X42otokoxkqBGeHw/s400/styled_todo_list.jpg" alt="" id="BLOGGER_PHOTO_ID_5033890127428106754" border="0" /></a>Much better, huh? The application looks completely different and we didn't have to touch any rendering code or a single line of html. I'll be the first to admit that my design skills aren't that inspiring--if someone comes up with something better they'd like to share, I'll post it here.<br /><br /><span style="font-size:130%;">Step 4: Tweak the WATableReport Row Colors</span><br />If you create more than 3 wanted items, you will see that yellowish background appear. That is WATableReport doing some styling for us. If we wanted to change it so that every other line was shaded with a light gray color, we would edit WantedList>>intialize by finding where the wantedReport instance is being assigned and changing it to the following:<br /><pre><br />wantedReport := WATableReport new rows: rows;<br /> columns: columns;<br /> yourself.<br />wantedReport rowPeriod: 1.<br />wantedReport rowColors: {'#ffffff'. '#eee'}<br /></pre><br />See David Shaffer's WATableReport <a href="http://www.shaffer-consulting.com/david/Seaside/WATableReport/index.html">tutorial</a> for more about styling the table report object. After clicking the "New Session" link, you should see the following:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFM4URQlNUWQdViMqnthNdHpKrTAqQUAmkskaDcv2V-SvAXv6OYEp2xJKwUwUGmOoiHcGbX4xBFArRCaqbo9TyD7O1OQj6SETwTzUNL7mR_0e7lCiit-d4x9Hrs9YDSS23YI8HBA/s1600-h/styled_todo_list_2.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFM4URQlNUWQdViMqnthNdHpKrTAqQUAmkskaDcv2V-SvAXv6OYEp2xJKwUwUGmOoiHcGbX4xBFArRCaqbo9TyD7O1OQj6SETwTzUNL7mR_0e7lCiit-d4x9Hrs9YDSS23YI8HBA/s400/styled_todo_list_2.jpg" alt="" id="BLOGGER_PHOTO_ID_5033890595579542034" border="0" /></a><br /><br /><span style="font-size:130%;">Conclusion</span><br />Making Wanted look decent was pretty simple using an external css file. Since Wanted really only consists of one table component and one form component, I was able to get away with not using divs, classes or ids. In an application with more depth these would be required.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com6tag:blogger.com,1999:blog-36815431.post-26732648825490217542007-02-17T04:12:00.000-06:002007-02-21T01:26:43.438-06:00Wanted -- A Seaside Tutorial: Part 5In the <a href="http://inchingforward.blogspot.com/2007/01/wanted-seaside-tutorial-part-4.html">last post</a> we created a constructor for the WantedItem class that made it easier to add WantedItems to the WantedDatabase in a workspace. In this entry we'll create a WantedEditor component that will allow us to create and edit WantedItems using the web app. Grab your beverage of choice and fire up Squeak with the <span style="font-weight: bold;">want-tutorial.image</span>.<br /><br /><span style="font-size:130%;">Step 1: Refactor the WantedItem Class</span><br />One of the things that the WantedItem <span style="font-weight: bold;">title:notes:</span> constructor does is initialize the enteredDate instance variable. If we create a WantedItem using the <span style="font-weight: bold;">new</span> message, the enteredDate will not get set. Let's change WantedItem to guarantee that any new WantedItem instance automatically gets an enteredDate. We'll do that by adding an <span style="font-weight: bold;">initialize</span> method to WantedItem:<br /><pre><br />initialize<br /> super initialize.<br /> enteredDate := DateAndTime now<br /></pre><br />Now that the initialize method handles initializing the enteredDate, we can take it out of the <span style="font-weight: bold;">title:notes:</span> constructor on the class side:<br /><pre><br />title: aTitle notes: someNotes<br /> ^ self new title: aTitle;<br /> notes: someNotes;<br /> yourself<br /></pre><br /><span style="font-size:130%;"><br />Step 2: Create the WantedEditor Class</span><br />Create a new class called <span style="font-weight: bold;">WantedEditor</span> that subclasses WTComponent and add a <span style="font-weight: bold;">wantedItem</span> instance variable:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifA0n6J8AIZmnAydrHL-5yA4vhXiSnFIRqXj05NvV5ZxhpBQsHrHIXO_mTcQNtZ5s1TDitPRWcWC-9xpSTLISO93j9klfCUXKFFFA7EEw1EJSqYzuLXLLkxg2VXDv50yyJN6k59Q/s1600-h/Wanted-Editor.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifA0n6J8AIZmnAydrHL-5yA4vhXiSnFIRqXj05NvV5ZxhpBQsHrHIXO_mTcQNtZ5s1TDitPRWcWC-9xpSTLISO93j9klfCUXKFFFA7EEw1EJSqYzuLXLLkxg2VXDv50yyJN6k59Q/s400/Wanted-Editor.jpg" alt="" id="BLOGGER_PHOTO_ID_5029406739916831138" border="0" /></a><br />Highlight the wantedItem instance variable, then right-click or option-click it and choose "selection...", then "create accessors". Change the wantedItem getter to the following:<br /><pre><br />wantedItem<br /> ^ wantedItem<br /> ifNil: [wantedItem := WantedItem new]<br /></pre><br />The purpose of WantedEditor is to either create a new WantedItem instance, or edit an exising one. Having a getter that lazy initializes wantedItem to a new WantedItem instance if it doesn't already exist allows for creation. Having a setter that allows us to set wantedItem to an existing instance allows for editing. We'll get to this in a bit.<br /><br /><span style="font-size:130%;">Call And Answer</span><br />Before we continue finishing the WantedEditor component, we need to learn how WantedList will be able to transfer control from itself to an instance of WantedEditor. Seaside provides the <a href="http://seaside.st/Documentation/CallandAnswer/">call and answer</a> messages for this. We'll have WantedList send itself the <span style="font-weight: bold;">call:</span> message with an instance of a WantedEditor as the argument. Control of the application will then switch to the WantedEditor instance which will display its interface and wait for user action. When WantedEditor is done with its work, it can revert control back to WantedList by sending itself the <span style="font-weight: bold;">answer:</span> message. The answer: message accepts an argument that is returned back to the site of the <span style="font-weight: bold;">call:</span>. This way a WantedList can ask a WantedEditor to create a WantedItem and return it back to the WantedList with the <span style="font-weight: bold;">answer:</span> message:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_aOnuExw-R1Mr5IINn-OK-FEajOTqKIQiSzPlQCOqnBH7ngdArE805vPGB6fp9ySa3CgGyt3vM_DYCWpl3Qz-J1kwXdBHbGnifa7ZrMoq3ZA7I-c7XW8CC3xQiIkGCo4D9ueoFg/s1600-h/call_and_answer.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_aOnuExw-R1Mr5IINn-OK-FEajOTqKIQiSzPlQCOqnBH7ngdArE805vPGB6fp9ySa3CgGyt3vM_DYCWpl3Qz-J1kwXdBHbGnifa7ZrMoq3ZA7I-c7XW8CC3xQiIkGCo4D9ueoFg/s400/call_and_answer.jpg" alt="" id="BLOGGER_PHOTO_ID_5031276128727382514" border="0" /></a><br />We're going to work a little backwards by implementing answer: before call: since we're already in WantedEditor. Let's start with creating WantedEditor's renderContentOn: method.<br /><pre><br />renderContentOn: html<br /> html<br /> form: [html text: 'Title: '.<br /> html textInput on: #title of: self wantedItem.<br /> html break.<br /> html text: 'Notes: '.<br /> html textArea on: #notes of: self wantedItem.<br /> html break.<br /> html submitButton on: #save of: self.<br /> html cancelButton on: #cancel of: self]<br /></pre><br />This will produce the following form:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUeUGD67UW8_dZ6OTrfptcdQVMNrsteaW3_O0qbHItQn9jmJWd_EYu5EuNPWfUjQFeeTgC-hPtGmbfP1WCeLLOOwlv8Xd2NB-Kzq4NBFFYuI-RRe77GiCou7c4dqugtkCzBp39Tg/s1600-h/wanted_editor_for_call_and_answer.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUeUGD67UW8_dZ6OTrfptcdQVMNrsteaW3_O0qbHItQn9jmJWd_EYu5EuNPWfUjQFeeTgC-hPtGmbfP1WCeLLOOwlv8Xd2NB-Kzq4NBFFYuI-RRe77GiCou7c4dqugtkCzBp39Tg/s400/wanted_editor_for_call_and_answer.jpg" alt="" id="BLOGGER_PHOTO_ID_5030498378869553618" border="0" /></a>The WantedEditor renders an html form that contains text form elements for title and notes and two submit buttons for saving and cancelling. Each html element is created by asking the html canvas for the control it wants, then sending the control the <span style="font-weight: bold;">on:of:</span> message.<br /><br />The <span style="font-weight: bold;">on:of:</span> messages wire each form element to a selector (the <span style="font-weight: bold;">on</span> argument) of an object (the <span style="font-weight: bold;">of </span>argument) when the form is submitted. For the buttons, this means sending the WantedEditor instance the <span style="font-weight: bold;">save</span> or <span style="font-weight: bold;">cancel</span> message. For the text elements, this means sending the value in the form element to the selector (<span style="font-weight: bold;">title</span> and <span style="font-weight: bold;">notes</span>) of "<span style="font-weight: bold;">self wantedItem</span>". Remember, if a wantedItem hasn't been set, a new instance of WantedItem will be created in the first call to "self wantedItem".<br /><br />We need to create the two methods that correspond to the selectors used for the buttons:<br /><pre><br />cancel<br /> self answer: nil<br /></pre><br />and:<br /><pre><br />save<br /> self answer: wantedItem<br /></pre><br />Notice that both methods end by sending the WantedEditor instance (self) the <span style="font-weight: bold;">answer:</span> message. Both return an object back to the calling site: cancel returns a nil object since nothing has changed or been created, and save returns the wantedItem we were editing. <br /><br />Now we need to wire up WantedList to WantedEditor.<br /><br /><span style="font-size:130%;">Step 3: Call WantedEditor to Add WantedItems</span><br />Back in WantedList, add a link to add new wanted items in renderContentOn:<br /><pre><br />renderContentOn: html<br /> html render: wantedReport.<br /> html anchor on: #add of: self<br /></pre><br />When a user clicks the link, the <span style="font-weight: bold;">add</span> message will be sent to self (the WantedList instance). Let's go ahead and create <span style="font-weight:bold;">add</span>:<br /><pre><br />add<br /> | wantedItem |<br /> wantedItem := self call: WantedEditor new.<br /> wantedItem<br /> ifNotNil: [WantedDatabase wantedItems add: wantedItem]<br /></pre><br />We start by creating a wantedItem temporary variable. We then send the WantedList instance the <span style="font-weight: bold;">call:</span> message with a new instance of WantedEditor. The WantedEditor will assume control of the web application and display the user the form that allows them to set the wantedItem instance attributes. The first call to "<span style="font-weight:bold;">self wantedItem</span>" will lazy initialize a new wantedItem instance (which will now have the enteredDate set). If the user clicks the Cancel button a nil object will be returned (<span style="font-weight: bold;">answer</span>ed). If the user clicks the Save button, the wantedItem instance they were editing will be returned (<span style="font-weight: bold;">answer</span>ed). The result of either cancel or save is assigned to our wantedItem temporary variable. If it is not nil (they pressed the Save button), we add it to the WantedDatabase wantedItems collection.<br /><br />Let's add a new WantedItem to make sure it works. Open a web browser to http://localhost:8080/seaside/wanted. Click the "Add" link, fill in the form, then click the "Save" button. You should be returned to the wanted list which now contains the new wanted item at the bottom. Woohoo!<br /><br /><span style="font-size:130%;">Step 4: Call WantedEditor to Edit WantedItems<br /></span>To let the user select a wanted item to edit, we'll turn the Titles of the wanted items into links that when clicked call a WantedEditor instance:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvfyzb9LKOm6BJ3HULTMvZVHJ0O0R9YiQOffFzUuUXop57vqPAlMe1SFwA5tBWHfI8F8qHzbE1ED3ZhiajtluW5D2SWnQhBXD2n4YjIxSXHZBMJDq9BChdEYyEEwoWxVIDVYr27Q/s1600-h/links.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvfyzb9LKOm6BJ3HULTMvZVHJ0O0R9YiQOffFzUuUXop57vqPAlMe1SFwA5tBWHfI8F8qHzbE1ED3ZhiajtluW5D2SWnQhBXD2n4YjIxSXHZBMJDq9BChdEYyEEwoWxVIDVYr27Q/s400/links.jpg" alt="" id="BLOGGER_PHOTO_ID_5030502699606653410" border="0" /></a><br />To turn the Title text into links, we'll need to revisit the initialize method of WantedList where we created the WATableReport. Change the line where the title WAReportColumn is being created by adding a clickBlock and returning yourself:<br /><pre><br />initialize<br /> | rows columns |<br /> super initialize.<br /> columns := OrderedCollection new <br /> add: ((WAReportColumn selector: #title title: 'Title')<br /> clickBlock: [:item | self edit: item];<br /> yourself);<br /><br /> add: (WAReportColumn selector: #enteredDate<br /> title: 'Entered Date');<br /><br /> add: (WAReportColumn<br /> renderBlock: [:item | item isPurchasable asString]<br /> title: 'Can Buy');<br /> yourself.<br /> rows := WantedDatabase wantedItems.<br /> wantedReport := WATableReport new rows: rows;<br /> columns: columns;<br /> yourself<br /></pre><br />The clickBlock will receive the WantedItem that was clicked on (we name the block argument "item"). The block sends the <span style="font-weight:bold;">edit</span> message with the item to self (the WantedList instance). We need to create the <span style="font-weight:bold;">edit</span> method:<br /><pre><br />edit: aWantedItem<br /> self call: (WantedEditor new wantedItem: aWantedItem;<br /> yourself)<br /></pre><br />Here we are <span style="font-weight: bold;">call</span>ing a new WantedEditor instance again, but this time we are setting the wantedItem instance instead of having a new one lazy initialized. When WantedEditor's renderContentOn: method is called, it will use the values from the wantedItem object we passed to it to fill in the form values.<br /><br />Go back to the browser and click on the "<span style="font-weight:bold;">New Session</span>" link at the bottom of the page. We have to do this since our WantedList instance has already been initialized--clicking the "New Session" link will force a new WantedList instance to intialize, creating a new wantedReport that has the clickBlock changes we made. Click on one of the title links and you should get the wanted editor page. If you edit the title and click Save you will be taken back to the wanted list page and the title for that wanted item should have changed in the wanted list.<br /><br />We're almost there: the last piece of functionality is to remove WantedItems.<br /><br /><span style="font-size:130%;">Step 5: Add Remove Functionality </span><br />To add remove functionality, we're going to add a "Remove" link for each wanted item row. To do this, we'll have to add another column to our wantedReport in the WantedList>>initialize method:<br /><pre><br />initialize<br /> | rows columns |<br /> super initialize.<br /> columns := OrderedCollection new <br /> add: ((WAReportColumn selector: #title title: 'Title')<br /> clickBlock: [:item | self edit: item];<br /> yourself);<br /><br /> add: (WAReportColumn selector: #enteredDate<br /> title: 'Entered Date');<br /><br /> add: (WAReportColumn<br /> renderBlock: [:item | item isPurchasable asString]<br /> title: 'Can Buy');<br /> <br /> add: (WAReportColumn new title: 'Remove';<br /> valueBlock: [:item | 'Remove'];<br /> clickBlock: [:item | self remove: item];<br /> yourself);<br /> yourself.<br /> <br /> rows := WantedDatabase wantedItems.<br /> wantedReport := WATableReport new rows: rows;<br /> columns: columns;<br /> yourself<br /></pre><br />We set the title to "Remove", set the value to render (the link text) to "Remove", and the action to take when clicked to sending self the <span style="font-weight:bold;">remove</span> message.<br /><br />Let's add the <span style="font-weight:bold;">remove:</span> method:<br /><pre><br />remove: aWantedItem<br /> WantedDatabase wantedItems remove: aWantedItem<br /></pre><br />This simply removes the WantedItem instance from the WantedDatabase wantedItems collection.<br /><br />Flip back to the browser and click the "New Session" link. You should see a "Remove" link next to each wanted item row. Go ahead and click on of them. The item should be removed from the WantedDatabase, then the page should re-render with the item removed from the table.<br /><br />And that's it! Go forth and buy less!<br /><br /><span style="font-size:130%;">Conclusion</span><br />At a basic level, this tutorial is complete. We have a first cut at a working application: we can Create, Report, Update and Delete WantedItems. The posts so far have shown how to build a simple application using the basic building blocks of Seaside. The app is by no means finished: it's ugly, it's not using the latest and greatest ideas and tools available for seaside (things like Magritte and Scriptaculous), there aren't any unit tests, we're using the image to store our items , we haven't provided a way to import or export the data, etc. Future posts will focus on some of these concerns, but may not use the Wanted material. Stay tuned!<br /><br /><span style="font-weight:bold;">Update</span><br />There was a bug in WantedEditor where pressing the Cancel button would persist any changes made to the title or notes fields. To change this, the Cancel "submitButton" has been changed to a "cancelButton". If you are using the same squeak-web image version downloaded in the first part of this tutorial or you do not have WARenderCanvas>>cancelButton, you will need to upgrade the Seaside version. Here's how to do it step by step:<br /><ol><br /><li>Left-click on the world and choose "open..." then "Monticello Browser".</li><br /><li>Click on the "Seaside2" entry in the left list pane.</li><br /><li>Click on the "http://www.squeaksource.com/Seaside" url entry on the right list pane so that it is highlighted.</li><br /><li>Click the Open button above the right list pane.</li><br /><li>Find and click on the entry marked "Seaside2.7a1-mb.142.mcz" in the right list pane so that it is highlighted. This was the commit that added the cancelButton functionality.</li><br /><li>Click the Load button above the right list pane.</li><br /><li>Close the Monticello windows and change the Cancel submitButton to a cancelButton in WantedEditor>>renderContentOn:.</li><br /></ol><br />I apologize for not finding this earlier.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com3tag:blogger.com,1999:blog-36815431.post-32598575755672746002007-01-27T13:15:00.000-06:002007-01-29T10:07:07.107-06:00Wanted -- A Seaside Tutorial: Part 4In the <a href="http://inchingforward.blogspot.com/2007/01/wanted-seaside-tutorial-part-3.html">last post</a> we created our WantedItem model object and built a preliminary main page that lists the WantedItems stored in our WantedDatabase. In this entry, we'll refactor the WantedItem class a little bit, create an html table to display the wanted items, and make it look better. As usual, for those playing at home, open Squeak with the <span style="font-weight: bold;">want-tutorial.image</span><br /><span style="font-size:130%;"><br />Step 1: Refactor the WantedItem Class</span><br />At this point, our WantedItem is just a data holder. As Ramon Leon <a href="http://onsmalltalk.com/programming/smalltalk/objects-classes-and-constructors-smalltalk-style/">points out</a>, having a constructor encapsulates how one should instantiate an object. Something else to notice is that the WantedItem class has an enteredDate, which is the date the WantedItem was created. We don't need to have the user assign this value--we know when a WantedItem object is created, so we can assign the value ourselves.<br /><br />Select the WantedItem class in the Refactoring Browser, and click on the class button. Then select the "-- all --" message category so that the method creation template displays in the code pane. Enter the following code:<br /><br /><pre><br />title: aTitle notes: someNotes<br /> ^ self new title: aTitle;<br /> notes: someNotes;<br /> enteredDate: DateAndTime now;<br /> yourself<br /></pre><br /><br />and save. We create a new WantedItem by passing it the new message, then set the fields on the new instance using the arguments the class message receives, then set the current date and time by passing the <span style="font-weight: bold;">now</span> message to the DateAndTime class. Notice that our new method went into a category named "<span style="font-weight: bold;">as yet unclassified</span>". Right-click or option-click the <span style="font-weight: bold;">title:notes:</span> selector in the message pane, select "<span style="font-weight: bold;">more...</span>", then "<span style="font-weight: bold;">change category...</span>". A large list of possible categories pops up. Choose "<span style="font-weight: bold;">instance creation</span>".<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLQWzkTb9kMHpIwJqOr9L4euF4wqmQw34R0RC-gu-B7h5SksSCTMhHMa8aMpXSAX1I6onsJ7YdIDQaWIkUzNCV1_1T27qX8CmrUKz7SchTLF8E4RXsl6Ut5h65xwRSlxXQKjZJOw/s1600-h/title-notes-change-category-menus.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLQWzkTb9kMHpIwJqOr9L4euF4wqmQw34R0RC-gu-B7h5SksSCTMhHMa8aMpXSAX1I6onsJ7YdIDQaWIkUzNCV1_1T27qX8CmrUKz7SchTLF8E4RXsl6Ut5h65xwRSlxXQKjZJOw/s320/title-notes-change-category-menus.jpg" alt="" id="BLOGGER_PHOTO_ID_5023303457175613458" border="0" /></a><br /><br />Now that we have a constructor that will create a new WantedItem with all the required fields, it's easier to create WantedItems and users of the class will have more confidence that they are creating an instance correctly.<br /><br />One of the goals of this project is to figure out what items we really want based on how long we hold a desire for them. A WantedItem object instance already knows the date on which it was entered. If we provide the WantedItem class a with a time threshold, the instances can calculate whether or not they have passed the time test of being a true wanted item.<br /><br />First we need to identify what the time threshold is. To do this, create a new instance side method on WantedItem called "<span style="font-weight: bold;">daysToWait</span>". Make sure the instance button is selected, then click on the "-- all --" message category and replace the code pane with the following:<br /><br /><pre><br />daysToWait<br /> ^ 14<br /></pre><br /><br />The daysToWait method returns the time threshold in days, which we have set to two weeks. The daysToWait method was put into the "as yet unclassified" category. Change the category to "<span style="font-weight: bold;">defaults</span>".<br /><br />We then create a method called isPurchasable:<br /><br /><pre><br />isPurchasable<br /> ^ (DateAndTime now - enteredDate) days >= self daysToWait<br /></pre><br /><br />The <span style="font-weight: bold;">isPurchasable</span> method takes the current DateAndTime and subtracts the enteredDate DateAndTime from it. If you look at the "<span style="font-weight: bold;">-</span>" method in the DateAndTime class you will see that it returns a <span style="font-weight: bold;">Duration</span> object. Duration conveniently has a "<span style="font-weight: bold;">days</span>" method, which we use to compare against our daysToWait (14). If the item has been around the number of days to wait or longer, we can withdraw some money from the bank and get in trouble with the significant other. You'll be fine--just point them to the isPurchasable method.<br /><br />Our WantedItem has grown a bit. Instead of just holding data, it now provides an obvious way for users to create it and knows whether it is a purchasable item or not.<br /><br /><span style="font-size:130%;"><br />Step 2: Create the HTML Table </span><br />On the main page of the Wanted app we want to display a list of wanted items: their titles, enteredDates and whether or not we can purchase them yet. Since we'll be displaying tabular data, it makes sense to use an html table. Seaside comes with a component that makes handling tables pretty easy: <span style="font-weight: bold;">WATableReport</span>. David Shaffer created a <a href="http://www.shaffer-consulting.com/david/Seaside/WATableReport/index.html">good tutorial on WATableReport</a> based on some emails sent to the <a href="http://www.seaside.st/Community/MailingList/">Seaside Mailing List</a> from Radoslav Hodnicak and Dan Winkler. Basically you supply a WATableReport object with a collection of columns and a collection row data objects, and it will build the table for you.<br /><br />Click on the WantedList class and make sure the instance button is selected. Add an instance variable named "wantedReport" and save:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2-faXOeKYwKnzHjO9kgX4UHy1AK0WJDacSm5tFV9LcEqBAfB5j7pIV_FhLdTPmebZr83Sw18IcDUY8fIFDm59h8qoSPicn9UqQhOL2BTZj8DxFfcy7zEI19m1sVC2ijBQJXUQ6w/s1600-h/wanted-list-wantedReport-instance.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2-faXOeKYwKnzHjO9kgX4UHy1AK0WJDacSm5tFV9LcEqBAfB5j7pIV_FhLdTPmebZr83Sw18IcDUY8fIFDm59h8qoSPicn9UqQhOL2BTZj8DxFfcy7zEI19m1sVC2ijBQJXUQ6w/s320/wanted-list-wantedReport-instance.jpg" alt="" id="BLOGGER_PHOTO_ID_5023303706283716642" border="0" /></a><br />Then click on the "-- all --" message category and replace the code pane with the <span style="font-weight: bold;">initialize</span> method:<br /><br /><pre><br />initialize<br /> | rows columns |<br /> super initialize.<br /> columns := OrderedCollection new<br /> add: (WAReportColumn selector: #title <br /> title: 'Title');<br /> add: (WAReportColumn selector: #enteredDate <br /> title: 'Entered Date');<br /> add: (WAReportColumn selector: #isPurchasable <br /> title: 'Can Buy');<br /> yourself.<br /> rows := WantedDatabase wantedItems.<br /> wantedReport := WATableReport new rows: rows;<br /> columns: columns;<br /> yourself<br /></pre><br /><br />and save. The first line is obviously the name of the initialize method. The second line contains two temp variables that exist for the life of the method--we introduce them to make it easier to work with the objects we will be using. Our WATableReport uses two collections to display data: a collection of WAReportColumns and a collection of row objects. The third line makes sure ancestors get initialized correctly. To create the columns, on the fourth line we create a new OrderedCollection by sending the OrderedCollection class the "new" message. Lines 5, 6, and 7 add WAReportColumns to that collection. Each WAReportColumn is constructed by sending the "<span style="font-weight: bold;">selector:title:</span>" class-side message. For <span style="font-weight: bold;">selector</span>, we pass a symbol (see <a href="http://inchingforward.blogspot.com/2006/10/grokking-grammar.html">this post</a> for links to articles on Smalltalk syntax) representing the name of the message selector to call on each row data object. For <span style="font-weight: bold;">title</span>, we pass in the column heading that we want to display at the top of the table. Why is that "yourself" message there on line 8? We couldn't put a period directly after the last WAReportColumn because it would be returned by the parentheses and assigned to the columns temp variable. So we add a cascade operator after the last WAReportColumn, which will return the OrderedCollection, then send it the "yourself" message which just returns itself (you can't have the cascade operator as the last token). Line 9 assigns the WantedItems collection from our WantedDatabase to our rows temp variable. Lines 10 and 11 construct the WATableReport object using the columns and rows temp variables and assign it to the wantedReport instance variable. Line 11 returns yourself so that the columns temp variable is not assigned to the wantedReport intance variable.<br /><br />Notice that when you saved the initialize method, it was assigned to the intialization category. When the WantedList component is created, the intialize method will automatically be called, building a WATableReport and assigning it to our wantedReport instance variable.<br /><br />Since WATableReport is doing the heavy lifting to create the table html, our WantedList>>renderContentOn: method gets much smaller. Replace the renderContentOn: code with this:<br /><br /><pre><br />renderContentOn: html<br /> html render: wantedReport<br /></pre><br /><br />and save. The renderContentOn: method just asks the wantedReport to render itself (build the html table).<br /><br />Since WATableReport is a descendent of WAComponent, we now have a subcomponent in our WantedList. Seaside expects components to tell it when they have child components through the <span style="font-weight: bold;">children</span> method. Add the following instance-side method to WantedList:<br /><br /><pre><br />children<br /> ^ Array with: wantedReport<br /></pre><br /><br />The children method gets assigned to the "<span style="font-weight: bold;">children</span>" message category.<br /><br /><br /><span style="font-size:130%;">Step 3: Test the Table<br /></span>We are now finally at a point where we can switch back to the browser and see what we've got. Point your browser to http://localhost:8080/seaside/wanted and you should see something like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiG32VjQh31OP3bS0NHkWlm2Ky2g2UE6gb2moSyUv-GCvNc18tB6bXfdzaX4KYYNZfxzTf81UFplgy685NeUT-eQUD22Bk33K1eUD5xy5Rt86xXTFnjY64AWj7uEEj3d5lYQExc1Q/s1600-h/wanted-table.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiG32VjQh31OP3bS0NHkWlm2Ky2g2UE6gb2moSyUv-GCvNc18tB6bXfdzaX4KYYNZfxzTf81UFplgy685NeUT-eQUD22Bk33K1eUD5xy5Rt86xXTFnjY64AWj7uEEj3d5lYQExc1Q/s400/wanted-table.jpg" alt="" id="BLOGGER_PHOTO_ID_5023476492818034754" border="0" /></a>So it's not the prettiest looking table in the world--we'll get to that in another post. Notice that the EnteredDate contains the date that we entered the item on, and that the "Can Buy" row value is "false". Let's test the isPurchasable method to see if it is working.<br /><br />Flip back to the Refactoring Browser and open WantedItem>>daysToWait. Change the return value to 1. Since the comparison in isPurchasable is a greater than or equal test, this should pass all WantedItems. Save, then flip back to the browser and refresh. You should see "true" now for the "Can Buy" row value:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiT6aswmbQOnXDFhTsEDdot-wAWfpuo_GJkEurdUYaJexEo8TGrj8wDSH739qPdUwr81k3tPDWt3w63jYfvyO9w0wOUVmvFke_VfEF3HmeBP1ssWAxEbu91VT-7tZKo8XmrQbvCyw/s1600-h/wanted-list-showing-true.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiT6aswmbQOnXDFhTsEDdot-wAWfpuo_GJkEurdUYaJexEo8TGrj8wDSH739qPdUwr81k3tPDWt3w63jYfvyO9w0wOUVmvFke_VfEF3HmeBP1ssWAxEbu91VT-7tZKo8XmrQbvCyw/s400/wanted-list-showing-true.jpg" alt="" id="BLOGGER_PHOTO_ID_5024749894786723938" border="0" /></a>Make sure to change the daysToWait method back to returning 14.<br /><br />What happens when we try to display multiple items in the table? What happens if we click on the column headers (they are links)?<br /><br />First, to test out how the table will look with multiple rows of data, let's create some more wanted items and add them to the WantedDatabase collection. We can use the new WantedItem constructor. Open a workspace and enter the following:<br /><br /><pre><br />WantedDatabase wantedItems<br />add: (WantedItem title: 'Big Screen TV' <br /> notes: 'Need this for the Wii');<br />add: (WantedItem title: 'Extra Wii Controller' <br /> notes: 'For playing with a friend').<br /></pre><br /><br />Then highlight and "do it". Flip back to the browser and refresh. You should now see three WantedItem rows. Note that if you click on the headings, the table will sort by that column value. WATableReport gives us that for free. Wait a minute--did you try clicking on the "Can Buy" header column? You should have seen this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlvvQzCPxTrn2YRMQ4Zl84oFr8IaIH1P_9k2mDhnZxX-Amq1PWQ8gHHfXlZpoMZaOv6WABXx3e4wBcfooOK0laNeKzPz3kbC5nQgDZNsnlHF3f-xO5vX-mDpO3j1vUfwG84UVRdA/s1600-h/message-not-understood-false.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlvvQzCPxTrn2YRMQ4Zl84oFr8IaIH1P_9k2mDhnZxX-Amq1PWQ8gHHfXlZpoMZaOv6WABXx3e4wBcfooOK0laNeKzPz3kbC5nQgDZNsnlHF3f-xO5vX-mDpO3j1vUfwG84UVRdA/s400/message-not-understood-false.jpg" alt="" id="BLOGGER_PHOTO_ID_5023838425417101394" border="0" /></a><br /><br />In order to sort the rows when we click the "Can Buy" header column, it looks like the <span style="font-weight:bold;"><=</span> comparison message is being used. When the isPurchasable method is called for each row data item, a Boolean value (False in this case) is returned. The Boolean object does not have the <span style="font-weight:bold;"><=</span> comparison message. Flip back to the WantedList>>initialize method. You can see that the WAReportColumn that is created for the "Can Buy" column is using the <span style="font-weight: bold;">selector:title:</span> constructor. This is what is causing our problem: the isPurchasable selector is returning a Boolean value. Fortunately, the WAReportColumn has another constructor: <span style="font-weight: bold;">renderBlock:title:</span>. For each data object in the rows collection, the renderBlock will be evaluated instead of calling a selector. Let's use this to return a String instead of a Boolean. Change the WAColumnReport constructor for the "Can Buy" column to this:<br /><br /><pre><br />(WAReportColumn<br /> renderBlock: [:item | item isPurchasable asString]<br /> title: 'Can Buy');<br /></pre><br /><br />When the renderBlock message is called, the row data object is passed into the block (we're calling it "item" here). We call the isPurchasable message off the item which will return a True or False Boolean object. We then send that Boolean object the asString message which will return either "true" or "false". Save the initialization method, flip back to the browser, and refresh. You should be able to sort all the columns now.<br /><br />That's it for this entry--make sure to save the <span style="font-weight: bold;">want-tutorial.image. </span>We're getting closer to having a working application. The next post will focus on creating and editing WantedItems by using an editor component. <span style="font-weight: bold;"><br /></span>mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com6tag:blogger.com,1999:blog-36815431.post-51259086130180544852007-01-19T03:00:00.000-06:002007-01-19T01:23:02.100-06:00Wanted -- A Seaside Tutorial: Part 3In this entry we'll create our wanted item model object, handle storing instances of it, and also create a main page that will display a list of them. If you're not running Squeak, open it with the <span style="font-weight: bold;">want-tutorial.image</span> image file we've been saving our changes to.<br /><br /><span style="font-size:130%;">Step 1: Create the Model</span><br />The Wanted model is pretty simple: a WantedItem object that contains 3 attributes:<ol><li>a title</li><li>notes about the item<br /></li><li>the date the item was entered</li></ol>With the Refactoring Browser open, click the <span style="font-weight: bold;">Wanted-Tutorial</span> package so that it is selected and the object creation template shows in the code pane. Replace <span style="font-weight: bold;">NameOfSubclass</span> with <span style="font-weight: bold;">WantedItem</span>. Inside the single quotes after <span style="font-weight: bold;">instanceVariableNames</span>:, enter <span style="font-weight: bold;">title</span>, <span style="font-weight: bold;">notes</span> and <span style="font-weight: bold;">enteredDate</span>. Save the new class definition. The class should appear in the class pane:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD_WyrY3XNJJ2417LUYRYmzJSqbUrDpHbyL0wbxAM2YS6Vqr9C4bySw8hZs8TAJt-zcWEqj6ATrWwEyu_DLzJjkiCqDE0ry6_dteuNXX3z9MBXQRLbUR1vOS6zmvEMBACSIAjtYA/s1600-h/wanted-item.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD_WyrY3XNJJ2417LUYRYmzJSqbUrDpHbyL0wbxAM2YS6Vqr9C4bySw8hZs8TAJt-zcWEqj6ATrWwEyu_DLzJjkiCqDE0ry6_dteuNXX3z9MBXQRLbUR1vOS6zmvEMBACSIAjtYA/s320/wanted-item.jpg" alt="" id="BLOGGER_PHOTO_ID_5017043011587252290" border="0" /></a><br />One of the nice things about the Refactoring Browser is that it can create accessor methods for instance variables. Double-click the <span style="font-weight: bold;">title</span> instance variable (inside the single quotes) so that it is highlighted, then right-click or option-click <span style="font-weight: bold;">title</span> and choose <span style="font-weight: bold;">selection...</span> then <span style="font-weight: bold;">create accessors</span>:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSIkTfTNKm0g5DwqbrtNs6tP3G582zfjICjc2ICtHaF8UvBnH6eM_DOWhHVVDsr8zeNgvQ5enktMiXueKYssJh4BzKvJds61rx4CQ9pAUd48L8BTZCYj2kAw2x230SJkYcYLqhqQ/s1600-h/create-accessors.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSIkTfTNKm0g5DwqbrtNs6tP3G582zfjICjc2ICtHaF8UvBnH6eM_DOWhHVVDsr8zeNgvQ5enktMiXueKYssJh4BzKvJds61rx4CQ9pAUd48L8BTZCYj2kAw2x230SJkYcYLqhqQ/s320/create-accessors.jpg" alt="" id="BLOGGER_PHOTO_ID_5017043149026205794" border="0" /></a><br />The Refactoring Browser should have created two additional methods in your WantedItem class: <span style="font-weight: bold;">title</span> and <span style="font-weight: bold;">title:</span>. These act as your getter and setter respectively. Whenever you see a colon in a message selector it means the message expects an argument--see the links in <a href="http://inchingforward.blogspot.com/2006/10/grokking-grammar.html">this post</a> for help on understanding Smalltalk syntax. Notice that the message category <span style="font-weight: bold;">accessing</span> was created and that our accessors were put into it. Create accessors for <span style="font-weight: bold;">notes</span> and <span style="font-weight: bold;">enteredDate </span>and save. We now have an initial version of our WantedItem class that we can start using.<br /><br /><span style="font-size:130%;">Step 2: Create an Object to Abstract Storage</span><br />One of the cool things about working with a Smalltalk image is that when you save the image, you save the state of all the objects in it. We can take advantage of this and use the image as a database by creating an object that will store our WantedItems as a collection. As long as you save the image each time before you exit it, all changes to the collection will be persisted. Once we get the application fleshed out and working the way we want, we'll move on to other types of storage.<br /><br />The storage object is simple: it holds a collection of wanted items in a class instance variable. A class method will allow us to retrieve the collection and make changes to it.<br /><br />Click the <span style="font-weight: bold;">Wanted-Tutorial</span> package so that it is selected and the object creation template shows in the code pane. Change <span style="font-weight: bold;">NameOfSubclass</span> to <span style="font-weight: bold;">WantedDatabase</span> and save. Click the <span style="font-weight: bold;">class</span> button in the class pane. The code pane should look like this:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiWPzPN6Iwa0LjaR8stRXUawMZ6piQ2-g5OGGPB1oe4UU1MnoRgnVsjMteweFL98wafZVXekdG4xY5wbjbOIWQ-JaDDz8BuvzMXGfW329ufpEWoClFbIFYuLF0KtpBC_7yCw-BGw/s1600-h/wanted-database-class-vars.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiWPzPN6Iwa0LjaR8stRXUawMZ6piQ2-g5OGGPB1oe4UU1MnoRgnVsjMteweFL98wafZVXekdG4xY5wbjbOIWQ-JaDDz8BuvzMXGfW329ufpEWoClFbIFYuLF0KtpBC_7yCw-BGw/s400/wanted-database-class-vars.jpg" alt="" id="BLOGGER_PHOTO_ID_5018593419945160578" border="0" /></a><br />Inside the single quotes after <span style="font-weight: bold;">instanceVariableNames:</span>, enter <span style="font-weight: bold;">wantedItems</span>:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh27mZUw9-TY7a5W20Hl2xzXjg5PHxUR7jzZh87N2P1QyYq6dw5ZrFnOwk806ksRq05f-OoasJP9rcYgi_1LSS7sqH-iAYcmGn-C7bANCuFF_ahDevJl54BpxrATnD-LkdBbclKpg/s1600-h/wanted-database-wanted-items-class-var.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh27mZUw9-TY7a5W20Hl2xzXjg5PHxUR7jzZh87N2P1QyYq6dw5ZrFnOwk806ksRq05f-OoasJP9rcYgi_1LSS7sqH-iAYcmGn-C7bANCuFF_ahDevJl54BpxrATnD-LkdBbclKpg/s400/wanted-database-wanted-items-class-var.jpg" alt="" id="BLOGGER_PHOTO_ID_5018593497254571922" border="0" /></a><br />and save.<br /><br />Now we need to create an accessor. With the class button still selected, click on the "<span style="font-weight: bold;">-- all --</span> " message category. Replace the method creation template in the code pane with the following:<br /><br /><pre><br />wantedItems<br /> ^ wantedItems ifNil: [wantedItems := OrderedCollection new]<br /></pre><br /><br />and save. Notice that the Refactoring Browser created a new message category "<span style="font-weight: bold;">as yet unclassified</span>". To keep things clean, right-click or option-click the "as yet unclassified" category, choose <span style="font-weight: bold;">rename...</span> and enter "<span style="font-weight: bold;">accessing</span>". We now have a very simple way of persisting our WantedItems: we ask the WantedDatabase for the wantedItems class instance variable by sending the wantedItems class message and add or make changes. Notice that if the variable has not been accessed before, a new empty OrderedCollection is created and assigned to the wantedItems variable before being returned.<br /><br /><br /><span style="font-size:130%;">Step 3: Create a Component for the Wanted List Page</span><br />Our main page in the Wanted application will display a list of saved WantedItems. From this page we will eventually provide links to add, delete, and edit WantedItems. Let's create a component to display the list.<br /><br />In the Refactoring Browser, click the Wanted-Tutorial package so that the object creation template is displayed in the code pane. Change <span style="font-weight: bold;">Object</span> to <span style="font-weight: bold;">WTComponent</span> and <span style="font-weight: bold;">NameOfSubclass</span> to <span style="font-weight: bold;">WantedList</span> and save the component. Since WantedList is a subclass of WTComponent, we do not have to add the class canBeRoot method: it is inherited. We want WantedList to render differently than WTComponent, so click on the "-- all --" item in the message category pane so that the method creation template displays in the code pane. Create the renderContentOn: method by entering the following into the code pane:<br /><br /><pre><br />renderContentOn: html<br /> WantedDatabase wantedItems<br /> do: [:wantedItem | html paragraph: wantedItem title]<br /></pre><br /><br />and save:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAdK8H4Og_J6Pq13GD6IKCKKh724CNw1AUx2qrh7nGjERbJu04WQc2PzXlFhyphenhyphenn-sIaDGzK4WyVgAwRqKp_vbSRcuCQ6KynnB4AYyK7CvZ3ZXzuvWvLa-hMIPMTdDGqnoO2kUFxwQ/s1600-h/wanted-list-render-content-on.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAdK8H4Og_J6Pq13GD6IKCKKh724CNw1AUx2qrh7nGjERbJu04WQc2PzXlFhyphenhyphenn-sIaDGzK4WyVgAwRqKp_vbSRcuCQ6KynnB4AYyK7CvZ3ZXzuvWvLa-hMIPMTdDGqnoO2kUFxwQ/s320/wanted-list-render-content-on.jpg" alt="" id="BLOGGER_PHOTO_ID_5021612313802813426" border="0" /></a><br />Our renderContentOn: method asks the WantedDatabase for the wanted items collection, then for each wanted item we ask the html context to emit the title of the item in a paragraph tag. This is very simplistic, but for now allows us to view our stored wanted items.<br /><br />Now flip back to your web browser and go to http://localhost:8080/seaside/config. Click on the <span style="font-weight: bold;">configure</span> link next to the wanted application entry point:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmWfF0YRLXoy-_Nvzr3fJ9PhMQyY4qSubiIM6RKBrQ6ey2tR9VB9VdFdvpbfOb5ITW_MTLxa9rZ79iiR9t7O6zqf8Y3OMrI9vG04PqmOwydedM98HMK4N1kZf52QL6rpabV2ThCA/s1600-h/wanted-application-configure.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmWfF0YRLXoy-_Nvzr3fJ9PhMQyY4qSubiIM6RKBrQ6ey2tR9VB9VdFdvpbfOb5ITW_MTLxa9rZ79iiR9t7O6zqf8Y3OMrI9vG04PqmOwydedM98HMK4N1kZf52QL6rpabV2ThCA/s320/wanted-application-configure.jpg" alt="" id="BLOGGER_PHOTO_ID_5017043076011761746" border="0" /></a><br />In the <span style="font-weight: bold;">General</span> section of the page, change the <span style="font-weight: bold;">Root Component</span> dropdown to <span style="font-weight: bold;">WantedList</span>:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDG0D84NZhKpZxVRrCBpl9VrGImLWgSp3PZA8451Ul3aXQ3mVU2j84fTqJ7ftySR5nvq7vpMIInJgfndkHltqpqOy2tU8zDsbakyPW2DjCHN4ew5E4S7SUA9WDX6l4HdrR81q5pA/s1600-h/wanted-list-in-dropdown.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDG0D84NZhKpZxVRrCBpl9VrGImLWgSp3PZA8451Ul3aXQ3mVU2j84fTqJ7ftySR5nvq7vpMIInJgfndkHltqpqOy2tU8zDsbakyPW2DjCHN4ew5E4S7SUA9WDX6l4HdrR81q5pA/s320/wanted-list-in-dropdown.jpg" alt="" id="BLOGGER_PHOTO_ID_5018585349701611346" border="0" /></a><br />and click the save button. After the page refreshes to notify you that the changes were saved, click the done button. This will take you back to the main config page. Click on the wanted link. You should see nothing since the only thing we are displaying are wanted items in paragraph tags, and we haven't created any. Let's create one.<br /><br /><span style="font-size:130%;">Step 4: Create a WantedItem to Display</span><br />Flip back to Squeak, left click on the world, choose <span style="font-weight: bold;">open...</span>:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPEWnCnkBbKzE7m3Ati1ifohTisRtUDcngQfsS5XiIRUEcpZLbltIcfdhYbHaSfVj49r60RqU53FGWl_SYEIuNDLdz0NhsPa4osYenqZ4WbIQNeDwbMvtB2RiCL-D4tJcMb-z0Kw/s1600-h/world-open-menu.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPEWnCnkBbKzE7m3Ati1ifohTisRtUDcngQfsS5XiIRUEcpZLbltIcfdhYbHaSfVj49r60RqU53FGWl_SYEIuNDLdz0NhsPa4osYenqZ4WbIQNeDwbMvtB2RiCL-D4tJcMb-z0Kw/s320/world-open-menu.jpg" alt="" id="BLOGGER_PHOTO_ID_5021612446946799618" border="0" /></a><br />then <span style="font-weight: bold;">Shout Workspace</span><span style="font-size:130%;">:<br /><span style="font-size:100%;"><br /></span></span><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjY5qYjkk9Pni5PotWOzCXjWiAZcZhSHJgh5Ej2MLa_tXeUvSm7LhGIC4R69wYNt04GaM3B5VJ_B8MvuKZzWIU7j_IV42scNuokc76_JY3H1O2OJth3u1M6XX5JiBi8K93tWMEzwA/s1600-h/open-shout-workspace-menu.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjY5qYjkk9Pni5PotWOzCXjWiAZcZhSHJgh5Ej2MLa_tXeUvSm7LhGIC4R69wYNt04GaM3B5VJ_B8MvuKZzWIU7j_IV42scNuokc76_JY3H1O2OJth3u1M6XX5JiBi8K93tWMEzwA/s320/open-shout-workspace-menu.jpg" alt="" id="BLOGGER_PHOTO_ID_5021612099054448578" border="0" /></a><br />You should see a window like this one:<span style="font-size:130%;"><span style="font-size:100%;"><span style="font-size:100%;"></span><br /><br /></span></span><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidCwQrWYOfvP4jwSCcSLkVLYnya8Iy-rnSBRtZ_XzT0AYvm-JSXHcdqbhdClB_sIhhEx7kWR2tUOf61xOoCgBtat65NWWwIz40nKv7RBKlT9rXUHR7Fqxz3wOp-2DXDRdiK0_MsQ/s1600-h/shout-workspace.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidCwQrWYOfvP4jwSCcSLkVLYnya8Iy-rnSBRtZ_XzT0AYvm-JSXHcdqbhdClB_sIhhEx7kWR2tUOf61xOoCgBtat65NWWwIz40nKv7RBKlT9rXUHR7Fqxz3wOp-2DXDRdiK0_MsQ/s320/shout-workspace.jpg" alt="" id="BLOGGER_PHOTO_ID_5021612245083336674" border="0" /></a><br />The workspace is kind of like a command line for your Smalltalk image with full access to all of the objects in it. You can evaluate code snippets, write scripts, start services, etc. The Shout Workspace adds some extra features like syntax coloring. We're going to use the workspace to add a WantedItem to our WantedDatabase. Enter the following code into the workspace:<br /><br /><pre><br />WantedDatabase wantedItems add:<br /> (WantedItem new<br /> title: 'Nintendo Wii';<br /> enteredDate: (DateAndTime now);<br /> notes: 'Get some exercise while having fun')<br /></pre><br /><br />On the first line we're asking the WantedDatabase for the wantedItems (which will be an empty OrderedCollection since this is the first time we're accessing it). We immediately send the collection the <span style="font-weight: bold;">add:</span> message, passing a new WantedItem as the argument. The wanted item is created inside the parentheses by passing the WantedItem class the new message, then setting the 3 instance variables. The semicolons are a Smalltalk language feature called the <span style="font-weight: bold;">cascade operator</span> that returns the receiver of the last message (again, see <a href="http://inchingforward.blogspot.com/2006/10/grokking-grammar.html">this post</a> for links to understanding the Smalltalk syntax). Highlight the entire block of code, then right-click or option-click and choose "do it":<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjftCVU69tWHiGPGTM09YGVFmbMi9pNuj-50K4P6CbD515_n6mBT-uedmCFWT9LZsgusH0KSdjFfkrjTuqgSv2GqPOF9ehSFB0ZXYxCUKDsB5dtvsd_MqM_MF1R5j94NY-BoM1GKQ/s1600-h/shout-workspace-doit.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjftCVU69tWHiGPGTM09YGVFmbMi9pNuj-50K4P6CbD515_n6mBT-uedmCFWT9LZsgusH0KSdjFfkrjTuqgSv2GqPOF9ehSFB0ZXYxCUKDsB5dtvsd_MqM_MF1R5j94NY-BoM1GKQ/s320/shout-workspace-doit.jpg" alt="" id="BLOGGER_PHOTO_ID_5021612184953794514" border="0" /></a><br /><br />To make sure that the item was added, append the following block on its own line to the workspace:<br /><br /><pre><br />WantedDatabase wantedItems size<br /></pre><br /><br />Highlight it in the workspace, right-click or option-click, then select "print it". You should see the number 1 printed to the right of the block of code, letting us know that our item was added to the collection.<br /><br />Now flip back to your browser and refresh. You should see the title of the item we just added in the workspace.<br /><br />Congratulations--we're one step closer to world domination! Make sure to save your image (<span style="font-weight:bold;">want-tutorial.image</span>). Note that by saving the image, you'll be saving the Wii WantedItem we created. If you exit Squeak then fire it back up and go back to the wanted seaside page, it will show up again. Even cooler: install Squeak on another machine (even under a different operating system), copy the tutorial image to it, open the image and point your web browser to the seaside wanted app. You will see the same results. We now have a completely portable platform-agnostic development environment and database for our project.<br /><br />In the next post, we'll look at tweaking the display of the WantedList component.mjhttp://www.blogger.com/profile/01534954159169119635noreply@blogger.com1