Introduction
This article is for Delphi developers who want to learn Prism. It covers the most significant changes to the old Delphi language
and the basics about the new parallel computing support.
Delphi Prism is a .NET language based upon Delphi. You might say it is the Delphi.NET. The name also stands for a Visual Studio extension
that makes coding in Prism comfortable. It is a Visual Studio Shell application, so it integrates into an existing VS 2008 (if it is installed
on the machine) or runs as a standalone IDE.
Why should you use Prism at all? First, there are Linux and Macintosh. Maybe you want to write platform independent code for Mono.
Second, desktop applications are not always adequate and Delphi's ISAPI support is not state of the art. Third, you might be switching
from native to managed development in general. Learning to use a new platform will be easier with a language you already know.
There's a fourth possible reason: Your applications are designed for multicore computers.
Delphi Prism is designed especially for heavy multithreading and high responsiveness.
In this absolute beginner's tutorial, we'll write an address book using the Lucene.NET library.
To introduce desktop and web at the same time, there'll be a WPF application to fill the search index and a web page to search it.
Index Builder: Your First WPF Project
Create a new Delphi WPF project. Visual Studio shows you the Forms Designer, where you can design the main dialog as usual.
Change to the code editor. Delphi developers will be surprised: instead of a unit
the code generator has declared a namespace
.
In the class definition, you find the new keywords partial
and method
. The latter replaces procedure
as well as function
, because .NET doesn't make a difference between those two. (Rather every method has a return value,
for procedures it is void
.) The name Create
for constructors is replaced by nothing at all: objects get created
with new
, just as in the C-like languages.
namespace LuceneTest;
interface
uses
System.Linq,
System.Windows,
System.Collections.ObjectModel,
System.Collections.Generic,
Lucene.Net.Index,
Lucene.Net.Store,
Lucene.Net.Search,
Lucene.Net.Documents,
Lucene.Net.Analysis.Standard,
Lucene.Net.QueryParsers;
type
Window1 = public partial class(System.Windows.Window)
private
method btnIndex_Click(sender: System.Object; e: RoutedEventArgs);
method btnSearch_Click(sender: System.Object; e: RoutedEventArgs);
method addDocument(name: String; address: String; writer: IndexWriter);
public
constructor;
end;
Let's begin with the address book. A list of names has to be filtered and added to the search index. The new way to search lists
looks quite confusing: the common Delphi developer is used to declare all variables above the procedure! But thanks to LINQ,
there are lists without a defined type (that means, not defined in code), which don't even have to be declared.
method Window1.BuildSearchIndex;
var
persons: Collection<person>;
directory: Directory;
writer: IndexWriter;
analyzer: StandardAnalyzer;
begin
persons := GetPersons;
var filteredPersons := from n in persons
where (not n.Blocked)
order by n
select n;
directory := FSDirectory.GetDirectory('L:\\Index', true);
analyzer := new StandardAnalyzer();
writer := new IndexWriter(directory, analyzer, true);
for each person in IEnumerable<person>(filteredPersons) do
begin
addDocument(person.Name, person.Address, writer);
end;
writer.Optimize();
writer.Close();
end;
Index Searcher: Your First Web Project
The newly built search index is to be searched by a web form. Create a new Delphi ASP.NET project!
Again, Visual Studio shows you a designer window. Open the code behind to meet another new keyword.
partial
splits the Delphi class into an editable file Default.aspx.pas and the designer generated file
Default.aspx.designer.pas. While you're typing ASP code, the designer fill its file with all necessary declarations.
Your file will rarely be touched by the designer, unless the generated code must be edited by you. Now, double-click a button
on the web form to insert an event handler.
By the way, this is a good point to introduce the double compare operator. You can make your code more readable using
"0.2 <= score < 0.5
" instead of the long version "0.2 <= score and score < 0.5
".
method _Default.btnFind_Click(sender: Object; e: EventArgs);
var
indexDirectory: String;
parser: QueryParser;
query: Query;
searcher: IndexSearcher;
searchResults: TopDocs;
currentDocument: Document;
lblResult: Label;
begin
parser := new QueryParser('name', new StandardAnalyzer());
query := parser.Parse(txtName.Text);
indexDirectory := ConfigurationManager.AppSettings['LuceneIndexDirectory'];
searcher := new IndexSearcher(FSDirectory.GetDirectory(indexDirectory, false));
searchResults := searcher.Search(query, nil, 10);
for each currentResult in searchResults.scoreDocs do
begin
currentDocument := searcher.Reader.Document(currentResult.doc);
lblResult := new Label();
lblResult.Text := string.Format("{0} - {1}, {2}",
ScoreToString(currentResult.score),
currentDocument.Get('name'),
currentDocument.Get('address'));
pResult.Controls.Add(lblResult);
end;
pResult.Visible := true;
end;
method _Default.ScoreToString(score: Single): String;
begin
Result := 'Impossible';
if(0.2 <= score < 0.5)then
Result := 'Improbable'
else if(0.5 <= score < 0.8)then
Result := 'Maybe'
else if(0.8 <= score < 1.0)then
Result := 'Quite sure'
else
Result := 'That is it!';
end;
More CPUs, Please!
Performance has always been an argument for using Delphi. Prism focuses on parallel programming and scores with handy new keywords which save lots of code and chaos.
The most handy one is async
which simply declares a method as asynchronous. An asynchronous method is executed
in a separate thread - automatically! That way a single word in the declaration line can prevent the main thread from being slowed
down by log output calls. In the declaration of a variable, async
is the promise that the variable will - in the future - contain a certain value.
The value itself is calculated in a separate thread. If another thread accesses the variable before the calculation is finished, that thread is blocked until
the value is available. But the most impressive news is the asynchronous block:
01 SomethingInTheMainThread(a);
02 var myBlock: future := async
03 begin
04 DoSomethingLong(b);
05 DoEvenMore(c);
06 end;
07 WholeLotInTheMainThread(d);
08 myBlock();
When is line 04 executed? Assuming there are enough processors, simultaneously with line 07.
async
makes the block run in another thread. The future
variable myBlock
is your handle for that thread,
so you can wait for it in line 08. The method call myBlock()
doesn't execute the block! Only if it hasn't yet finished, it waits for the thread to end.
Inside the asynchronous blocks, you may call other asynchronous methods. But be careful: the main thread doesn't wait for them.
Waiting for an asynchronous block means waiting for this one thread, not for any "nested" threads that have been started by the block.
To visualise all that parallel stuff, the web page contains four checkboxes. They'll be evaluated in the background, when the user already sees the response page in his browser.
type
_Default = public partial class(System.Web.UI.Page)
protected
method btnFind_Click(sender: Object; e: EventArgs);
method btnSurvey_Click(sender: Object; e: EventArgs);
private
logFile: String;
logWriter: future StreamWriter := new StreamWriter(logFile);
method ScoreToString(score: Single): String;
method WriteLog(i: Integer; text: String); async;
end;
implementation
method _Default.btnSurvey_Click(sender: Object; e: EventArgs);
begin
logFile := ConfigurationManager.AppSettings['LogFile'];
logWriter.AutoFlush := true;
var backgroundWork: future := async
begin
logWriter.WriteLine("Loop starts at {0:mm:ss:fff}.", DateTime.Now);
for each matching chkSurvey: CheckBox in pSurvey.Controls index i do
if(chkSurvey.Checked)then
WriteLog(i, chkSurvey.Text);
logWriter.WriteLine("Loop stops at {0:mm:ss:fff}.", DateTime.Now);
end;
backgroundWork();
logWriter.WriteLine("Page delivered at {0:mm:ss:fff}.", DateTime.Now);
end;
method _Default.WriteLog(i: Integer; text: String);
begin
Thread.Sleep(1000);
logWriter.WriteLine("{0}. Selection is '{1}' at {2:mm:ss:fff}.", i, text, DateTime.Now);
end;
The StreamWriter
seems to be initialised with an empty path. But the keyword future
causes the assignment
in the declaration line to be executed shortly before the first access to the object. At that time, the path already is assigned.
The method WriteLog
is async
, so it always runs in the background.
In the async
block's thread, a for each matching
loop iterates through all controls that are checkboxes.
Every iteration can call another asynchronous method. When in the end the main thread waits for the block to finish, it waits only
for the block's thread, not for any additional thread started by the block. The WriteLog
calls can still run in the background,
maybe waiting for a free CPU, when the user already sees the complete response.
The Source
To compile the demo applications, you need the assembly Lucene.Net.dll.
If the latest stable release doesn't work, try version 2.0.004.
A 30 days trial version of Delphi Prism can be downloaded from Embarcadero.
Sadly, they don't offer a free Express version.