|
|
Index: Home | What Is Izumi | Misc Links | Random Thoughts | Too Much To Read | The Rant Vault | Quotes Dev: Projects | Ideas For Dev | Nerdkill | Rig | Hint
$Id: Rivet.izu,v 1.2 2005/12/19 05:46:49 ralf Exp $
20050426
Rivet stands for "Ralf Image Viewer, Exif and Transfer" (*).
Summary: A desktop application to manipulate a library of images, download them for a camera, and synchronize repositories with RIG.
The detailed goal of Rivet is to:
Implementation:
20050426
Milestones:
1.3.0 M0: View and Download
20050426
1.3.1 M1: Exif and renaming
20050426
1.3.2 M2: RIG upload
20050426
How exactly this is going to happen remains to be seen:
2.1. M0: Initial prototype
To be done (by decreasing priority):
Finished (by decreasing date):
(Notes are given in chronological order.)
Basic architecture, from a class point of view:
Note about UI:
Note about cache:
The absolute minimum for a prototype to do anything useful:
Expansion:
RMainFormEx override RMainForm and contains all the logic of the UI.
UI elements are declared (via the VS.Net resource editor) in RMainForm.
As much of possible all actions performed by the UI are implemented in
RMainFormEx rather than directly in RMainForm.
The goal is to separate the pure UI widget management from the logic of the window,
in the hope it may be easier later to use a different UI kit (GTK#, etc.)
In a first test, RMainFormEx directly derives from RMainForm and thus will
access WinForm widgets directly.
Note that the distinction used here is more or less similar to what VS.Net 2005
forces one to do with partial classes for WinForms and C# 2.0.
I setup my NUnit testing by having a separate Console Application that contains
a main that invokes the NUnit testing framework on a set of listed libraries.
This separate project also contains the NUnit classes, so I have a library Foo
containing a class Bar, there will be a class TestBar in the TestConsole project.
Obviously TestBar will need to import Bar, thus the library Foo must be referenced
in the TestConsole project. So far so good.
Unfortunately, I cannot reference an EXE this way. This means that I cannot
directly have a NUnit class which test functionnality located in the main application.
OTOH when I created the skeleton I kind of arbitrarily though it would be good
for every application (i.e. project producing an EXE) to have a sidekick class
library (producing a DLL) and has much as possible move all the logic in the
DLL and keep only the minimum UI in the main EXE. The idea was that once the
application gets finalized, fixes will probably be either in the logic part or
the UI part and maybe only one of those would need updating. Of course there's
still a pretty tight coupling between those two.
So I just started doing just that, by experimenting with the following
layout:
With this layout, I can have both RMainFormEx and RAppState in the library,
only RMainForm needs to be in the application project.
Actually that's not even true as there is another class, RMainModule, which is
the only one that really needs to be in the application project. It could
reference RMainForm from the library. So later I may want to do that -- move
everything in the library and only keep a minimal loader/boostrap/main entry
point in the application's EXE. Right now this is mostly irrelevant.
Anyway I started with RMainFormEx placed in the main application project,
next to RMainForm. Now I want to adapt the layout as described above.
There's one more problem: I can't have RMainForm in the application and
RMainFormEx in the library with RMainFormEx inheriting from RMainForm.
The reason is simply because the library cannot reference the main application
(it's an EXE, not a DLL, and anyway if both depend on each other that makes
circular references... not a good thing.)
Basically I cannot have this:
The solution is to introduce an abstract interface that defines the
callbacks uses in RMainForm and implemented in RMainFormEx, like this:
Of course, it becomes just much easier to move RMainForm in the library
and not use an extract abstract interface which is merely a workaround:
This is still not completly satisfactory.
The thing is that the abstract interface does not add anything substancial
except the need to declare callbacks in three places instead of two.
The interface is a workaround to a project's reference issue, not a design
issue. And anything that reduces the amount of code needed reduces the
potential for errors, so it's a good thing.
RMainForm and RMainFormEx will always have a tight coupling, that is I would
have used partial classes if I had been coding this in C# 2.0.
On the other hand it forces me to put all the UI stuff in the library
whereas I said before I wanted to have only the "logic" part in there.
So I want it in a library but not in the library. Hmm?
The obvious solution is to use two libraries:
Now the RMainModule is accessed by the forms to get the global form instances
via a centralized class rather than by cross-referencing them (i.e. to get the
main form from the pref window, ask the main module about the form instead of
having a global variable with the form.) This way I can avoid global variables
completly by having static methods in the main module return the main form.
If I were to move all forms in the UI library, they couldn't access the main
module class (since the library can't reference the application.)
So I need to move the RMainModule class in the UI library too.
All what's left in the application project is a minimal class with a static
main that instantiates the RMainModule class. This one in turn will create
the main form. RMainModule should be a singleton class.
So at the end we get this layout:
Now the only thing is that it is important to avoid cross or circular references
between libraries. References between the libraries should always be able to be
represented by a tree with no backwards links.
This is easily enforces since VS.Net will not let you reference a library that
may cause a circular reference.
In this case we have the following dependency graph for the various libraires
and applications:
I implemented a bunch of stuff almost as planned today: RDir, RImage,
both deriving from RFSItem (instead of RFileItem), loading directories
and files (with a regexp filter), current selection in library and
directory and displaying the images as a large icon list view.
That last one was a quick prototype hack. I will need to replace the ListView
by a custom view. The ListView display only 8-bpp images by default .Net 1.1
and in the end I will want something more fancy than what a list view can do.
But for a first prototype, it's more than OK.
The simple design I planned with RFileItem (now called RFSItem), RDir and
RImage worked just fine. Yet I had forgotten something which is platform
specific: if the image is going to contain a thumbnail stored as a bitmap,
it needs a way to dispose of the bitmap when no longer needed.
The alternative offered by .Net is to dispose of the resources in an overridden
Finalize()
methods. This is discourages as there are many limitations and issues
associated with finalization (order is not guaranteed, cross-object references
are not guaranteed, the thread is unknown, etc.) Disposing of a GDI+ resource in
Finalize() is definitely a bad idea. As the documentation for Finalize() says,
if one needs control another scheme should be used and IDisposable offers exactly
that. Control here means that it is up to the application's client code to
take care of disposing things in the right order, which is a lot easier than
it may seem:
Also it is safe for each dispose method to set the manipulated object
reference to null (f.ex. the image will set the thumbnail reference to null).
This means it is safe to dispose of the same object several times (probably by
mistake or bad design.)
This way, if the Dispose() method is called but then the resource is accessed
again, it will be correctly reloaded. This works because accessing a resource
via a property get will load the corresponding resource, of course.
This scheme could be implemented on purpose, for example to free resources
that have not been used for a while (typically when several libraries of images
have been loaded by the user, it may make sense to dispose of the other loaded
libraries after a timeout.)
So overall that's a nice addition to the design even though I completely missed
at first.
It's rather implementation specific too. Actually is it?
I don't remember seeing a IDisposable interface used so prominently in Java,
for example. In this case, the need for it arose from the fact that GDI+ resources
need to be disposed of properly and this may be true of other interop
resources. But in fact the same scheme can be used as a way to free memory
temporarily. The resource could be a file descriptor object that one may want
to close after a certain delay of inactivity. Or in this application I'd like
to unload the data of all but the current library.
It makes a good pattern too: objects that are loaded on demand and unloaded
when not necessary. Although the design is radically different, this is very
close to the example given as a motivation behind
Proxy(207) in
Design Patterns
yet it is actually more an example of the
Lazy Instantiation Pattern
with the added motivation of release the "hungry" resource at any time.
Now for the tricky unresolved (yet) question: how do I write a NUnit test
for a class that automatically loads a resource when accessing a property
and dispose it later, using solely its public interface? Assuming I have a
test that check the resource is loaded correctly, if I dispose it by calling
Dispose() I can't then use the property to check it has been disposed
since that would load the resource again...
So a few days ago I create the custom control to display the thumbnails
for the current selected directory.
I hacked a way to get the thumbnails and ended up with a bunch of useless
code and an horrible implementation. I'm not sure what I was thinking
but it was definitely the wrong way to do it:
As you can image the result is incredibly unusable. Loading a directory with a
dozen images starts eating memory like hell. That's because all images in the
directory get loaded in memory and never released -- why, I never bothered
writing any code that would unload them after a while! And anyway, I don't
need to have more than one image loaded in memory.
Plus the whole thing is completly blocking.
So let's think about the right way to do it.
First, on the RImage side:
On the image view side, I need asynchronous processing.
The image view is first cleared, then image items are loaded and the view
is refreshed to start drawing.
From a user's perspective, I want to see items appear as they get loaded.
At first the view already knows its target thumbnail size and the leaf name of each
image so it can display an empty rectangle for each image and their name.
Once this is done, it can start fetching the thumbnail and the size for each
image. Now I know that I don't really need the size before anything else, so
coming back to the RImage, I can simply load the thumbnail and everything
at once, including the EXIF tags.
Now coming back to the album, when changing albums and coming back to the same
album, it would be nice if the image view didn't have to reload all the
information.
The simpliest way to achieve that would be to keep the hashtable of ImgInfo
around for the previous albums. Then either on a timer or on a size threshold
(number of items or memory), older albums should be removed to avoid
keeping too much stuff around in memory.
Resources:
20050722 [1.N] Classes: RImagesView, add & manage scrollbar
20050722 [1.N] Classes: RImagesView, display image name
20050717 [1.N] Classes: RImage, load/store all EXIF info
20050717 [1.N] Classes: RImagesView, store ImgInfo in hashtable specific to current album
20050717 [1.N] Classes: RImagesView, expire hashtable ImgInfo for albums too old
20050521 [1.N] Project: Add WIA reference
20050521 [1.N] Classes: RWiaImport, skeleton, basic import in currently selected directory
20050521 [1.N] Classes: Refresh image view after import
20050521 [1.N] Classes: RDirViewControl, ability to select an image
20050521 [1.N] Classes: RMainWindow, add info panel
20050521 [1.N] Classes: RMainWindow, display info from current selection
20050521 [1.N] Classes: RMainWindow, add rename panel (using regexp and replace)
20050521 [1.N] Classes: RRenameTool, skeleton
20050521 [1.N] Classes: RRenameTool, rename existing items from current dir using panel info
20050521 [1.N] Classes: Refresh image view after import
20050521 [2.N] Classes: RImage, implement cache with invalidation
20050722 [1.F] Classes: RImagesView, abort asynchronous operation when album changed or resized
20050721 [1.N] Classes: RImagesView, update singular image items when load completed
20050721 [1.F] Classes: RImagesView, start asynchronous load of image items
20050717 [1.F] Classes: RImagesView, display empty image placeholders
20050717 [1.F] Classes: RImage, load/store thumbnail, size, name (metadata)
20050712 [1.F] Classes: RImagesView, display image list
20050712 [1.F] Classes: RImage, implemented crude creation of thumbnail & get size
20050712 [1.F] Classes: RMainForm+Ex, Using RImagesView instead of WinForms\' ListView
20050626 [1.F] Classes: Created RImagesView (custom control, ex-RDirViewControl)
20050530 [1.F] Classes: Implemented IDisposable for RImage, RDir, RFSItem, RAppState
20050530 [1.F] Classes: RMainFormEx, display thumbnail in list view in large icon mode
20050530 [1.F] Classes: RImage, return Thumbnail for image (computed once only)
20050530 [1.F] Classes: RMainForm+Ex, load directory tree for current library
20050530 [1.F] Classes: RDir, load directories and images with filter
20050530 [1.F] Classes: RFSItem, properties Name, Parent, FullPath
20050530 [1.F] Classes: Added RFSItem, RDir, RImage skeletons with NUnit test
20050529 [1.F] Classes: Added missing LibVersion in RivetLib and RivetUI
20050529 [1.F] Classes: RMain added in RivetApp to load RivetUI.RMainModule
20050529 [1.F] Classes: Reorganized, adding RivetUI lib with all forms and RMainModule
20050528 [1.F] Classes: RMainForm+Ex, Load last current library at startup
20050524 [1.F] Classes: RAppState, create skeleton with Libraries and CurrentLibrary
20050524 [1.F] Classes: RLibrary, created skeleton with NUnit and AbsRootPath
20050524 [1.F] Classes: RMainFormEx, added location combo box management
20050522 [1.F] Classes: RMainFormEx, implement logic part of RMainForm
20050522 [1.F] Classes: Load/save window pos for RDebugForm/RPrefForm
20050522 [1.F] Classes: RPrefForm, empty skeleton extracted from Xeres
20050522 [1.F] Classes: RMainForm, create basic UI (tree control, base folder)
20050522 [1.F] Project: Use AppSkeleton
2.2. Milestone 1
2.3. Milestone 2
3. Notes
Rivet stands for "Ralf Image Viewer, Exif and Transfer".
Yes, I have to admit I came up with the name first and tried to retrofit
an acronym to it. Got a problem with that? :-)
[ .Net ] [ Application ] [ Library Project ]
[ Project ] [ ]
Windows.Form
|
+--------<|---------+
|
+-------------+ +-------------+
| RMainForm | | RAppState |
+-------------+ +-------------+
| |
+---------<|---------+ |
| |
+-------------+ |
| RMainFormEx |- -(uses)- -+
+-------------+
+--------------+ |
| RMainModule |- -(uses)- -+
+--------------+
[ .Net ] [ Application ] [ Library Project ]
[ Project ] [ ]
+-------------+
Windows.Form | RIMainForm |
| +-------------+
| |
| |
+--------<|---------+---------|>---------+
|
+-------------+ +-------------+
| RMainForm | | RAppState |
+-------------+ +-------------+
| |
+---------<|---------+ |
| |
+-------------+ |
| RMainFormEx |- -(uses)- -+
+-------------+
|
|
+--------------+ |
| RMainModule |- -(uses)- -+
+--------------+
[ .Net ] [ Application ] [ Library Project ]
[ Project ] [ ]
Windows.Form
|
+-----------------<|---------------------+
|
+-------------+ +-------------+
| RMainForm | | RAppState |
+-------------+ +-------------+
| |
^ |
| |
+-------------+ |
| RMainFormEx |- -(uses)- -+
+-------------+
+--------------+ |
| RMainModule |- -(uses)- -+
+--------------+
[ .Net ] [ Application ] [ UI Library ] [ Library ]
[ Project ] [ Project ] [ Project ]
Windows.Form
|
+-----------------<|---------------------+
|
+-------------+ +-------------+
| RMainForm | | RAppState |
+-------------+ +-------------+
| |
^ |
| |
+-------------+ |
| RMainFormEx |- -(uses)- -+
+-------------+
|
(uses)
|
+-------------+
| RMainModule |
+-------------+
+--------------+ |
| RMain |- -(uses)- -+
+--------------+
LibUtils
|
+--------------------------+
| |
| v
| LibUtilsTests
v |
RivetLib |
| |
+------------------+ |
| | |
| RivetLibTests |
v | |
RivetUI | |
| | |
+-----------+ | |
| | | |
| RivetUITests | |
| | | |
| +------+-------+
| |
v v
RivetApp TestsConsole
This is simply achieved by implementing
IDisposable
and freeing the bitmap resource in
Dispose()
yet it up to the client code to take care of that.
Obviously the solution is to trick the class into loading a resource which
the unit test can check. For example that resource could be an object
which instance is know to the test class and the test class can check some
counter changed when the instance is created or disposed.
The problem I have with that is that it forces the tested class to be
redesigned in order to make it testable. Thus some complexity is added
just for the test whereas that complexity would not need to be there if there
was no testing done... Hmm?
Generally speaking I want to avoid spending time to get all infos I might need
unless I really need them. On the other hand, I don't want to load an image twice
just because two set of informations are not collected in the same loop.
There are two possible approaches:

This work is licensed by Raphaël Moll under a Creative Commons License.
Color Theme:
Gray
| Blue
| Black | Sand
| Khaki
| Egg
| None
459 accesses, 1 access from 38.107.179.208
Visited 4 times by Google, last 2011/10/24 09:16
Visited 13 times by Yahoo!, last 2011/10/10 14:55
Visited 1 times by MSN, last 2011/06/17 06:41