Introduction
With the "success" of the first project and some spare time, I start a completely new version hosted on the same CodePlex project.
I decided to rewrite it to explore a bit more about the MVVM pattern and extend it to another ebook. I am also interested in 7 phone development,
so I took the idea of dynamic books that I have seen on an iPhone application. The roadmap includes:
- Better user interface and design: Ribbon...
- Multiple format support and conversion: Images, PDF, XPS, CBR/RAR, CBZ/ZIP, ePUB...
- Dynamic books format: CBZD, a complete zip format with additional XML files associated to pages that contain frame descriptions. CBR contains a designer
to "edit" books to add frames on the different page zone with order and timing. This allows automatic reading usable on desktop, but mainly for phone application.
- External devices support and a Windows Phone 7 application.
Related publications: CodePlex, "Product" site,
Blog

Screenshot: New C.B.R. interface
Project content
As a developer, what you can find helpful in this project, despite a lot of tricks that are not described here:
- New : Two page book view based on Mitsu Futura "book"
- Localization engine that works with resx, xml, bin and that you can extend to your needs...!
- An explorer view with grid, simple and complex thumb view
- A starting home page that display direct actions and "xml news" feed
- Quite complex MVVM pattern
- Simple explorer like view in MVVM with tree and folder view
- USB device detection with WMI
- PDF image extraction
- SevenZipSharp: extraction, compression, in memory usage
- Office like: Ribbon control usage, backstage design like recent file lists
- Controls: Magnify glass, 5 stars rating, Splitter expander, WaitSpin,
new ZoomFlyer
- XPS reading and writing, ePUB parsing and conversion
Screenshots
Backstage file information panel

Backstage converter panel

Backstage application settings

Backstage recent file list "a la" Office

Backstage device info that allows to manage supported devices

USB device view with explorer

1 page comic book view with grouped thumbnails

XPS book view with grouped thumbnails

ePUB book view with simple thumbnails

Home and grid view with group

Simulation window

The Code: Schemas and Principles
Has the project becomes a bit more complicated than the original and as it is always in beta stage, I split the article into several "high level"
parts: core classes, MVVM pattern classes and coding tips... until the architecture becomes stable to give a more complete description.
Model Core Classes
The model core classes illustrated below contain a Catalog class that represent a library. It is mapped to the ExplorerViewModel
and managed by a corresponding CatalogService. All books are represented by a Book entity, that extend (when possible) to Pages.
Zone class is dedicated to my new format for dynamic books. When extra data is needed, it is stored in the Tag property
of the Book (like for ePUB format).

Diagram: Model core classes
Service Core Classes
Below are the main service classes. FileService and FileExtension manage the data we need to associate a file (based on the extension) to data
like the dialog filter, but also the associated model, service and viewmodel. This will be used by the BookServiceFactory to create the corresponding
ViewModel that will also create the corresponding service using reflection. BookInfoService is a separated class that only manages the load/save
operation on Book binary internal structure. CatalogService is the class that manages the library.
Note: About the internal file structure...C.B.R. does not use any database. It is a single binary file that you can store where you want which
centralizes the catalog information (path to the book files). Underneath, in the current application folder, I store book information like rating or cover into a separate
bin file, so that they can be shared among several libraries. You can lose you library, if the book binary is there, then nothing lost!

Diagram: Service core classes
Workspace Core Classes
These classes manage all data for the working environment. The singleton class WorkspaceService stores the settings in a WorkspaceInfo
class that contains program options (that the user can change in the backstage), two lists of RecentFileInfo for book and library recent file list and
the supported device list as a DeviceInfo collection. The settings class is serialized through the program properties. It is loaded a program start
and automatically saved on closing.

Diagram: Workspace core classes
Convert Core Classes
My wish was to not include any "third party" that is not pure WPF... so I had to find a way to convert from unsupported format (like PDF) to the one I choose to support.
Based on the convert panel in the backstage, we have a ContractParameter class that groups all chosen options. It is given
to the BookFileConverter class that supports threading through a BackgroundWorker (as it uses a report progress in the user interface).
Then the conversion process goes through two interfaces that are based on input and output formats: IReaderContract and IWriterContract.
Note that the conversion process does not manage multiple input/output formats.
The BookFileConverter is first calling the reader to extract the useful data that goes through the writer. The package includes ImageFileReader,
RARImageReader, PDFImageReader, XPSImageReader for reading images in folder, Rar/Zip, PDF or XPS files (will be soon extended to ePUB).
We can write to Image files, a XPS or CBZ/ZIP file through ImageFileWriter, XPSImageWriter and ZIPWriter classes.

Diagram: Converter core classes
The standard way to transfer data from reader to writer is an array of bytes that represent images, but I take a shortcut for conversion like RAR to images
where the reader is extracting images from the RAR directly to a folder without the need of a writer. Below is the transfer table mode used for the conversion:
| Source |
Reader |
Transfer mode |
Writer |
Destination |
| Images |
x |
x |
x |
Images |
|
x |
direct |
compress folder |
CBZ/ZIP |
|
extract to mem |
mem |
write mem |
XPS |
| PDF |
extract to mem |
mem |
write files |
Images |
|
extract to mem |
mem |
write files compress folder |
CBZ/ZIP |
|
extract to mem |
mem |
write mem |
XPS |
| CBR/RAR |
extract to folder |
direct |
x |
Images |
|
extract to folder |
to temp |
compress folder |
CBZ/ZIP |
|
extract to mem |
mem |
write mem |
XPS |
| CBZ/ZIP |
extract to folder |
direct |
x |
Images |
|
x |
x |
x |
CBZ/ZIP |
|
extract to mem |
mem |
write mem |
XPS |
| XPS |
extract to mem |
mem |
write files |
Images |
|
extract to mem |
mem |
write files compress folder |
CBZ/ZIP |
|
x |
x |
x |
XPS |
| ePUB |
x |
x |
x |
Images |
|
x |
x |
x |
CBZ/ZIP |
|
extract to mem |
mem |
write mem |
XPS |
MVVM Related Application Classes
This is the most complicated part... below are the classes from the application that participate in the pattern. I am not going to describe the helper
classes like the ViewModelBase, the Mediator or the Messenger. It is cut in 5 parts. The views: main user interface,
the backstage and out of the pattern classes. The viewmodels: main user interface, the backstage and some additional classes. Let's go through them.

Diagram: MVVM application classes
View Layer
The main user interface is composed by a MainView (the whole window), an ExplorerView (the library explorer on the left) and some ribbon
content (different book format and device views) that are hosted by a TabConbtrol (with no user interface displayed) binded to a collection of ViewModel.
InfoView, OptionsView, RecentFileView and DeviceConfigView are panels displayed in the ribbon backstage.
BookView (for comics), XpsBookView (for XPS documents) and ePUBBookView (for web like viewer) are hosted in a TabControl in the main window part
USBDeviceView and later PhoneDeviceView are also hosted in a TabControl in the main window part and but displayed in a contextual manner by
the ribbon group "Devices"
The ConvertView and SimulateView are actually out of the pattern because of the BackgroundWorder/thread or a beta stage code.
ViewModel Layer
We have exactly the same for the view model layer, plus a BookViewModelBase that centralizes all common book functions and a DeviceViewModelBase that do
the same for devices. Note that they are all inherited from ViewModelBase (that host a Model in a Data property) which comes from
a BindableObject that implements the INotifyPropertyChanged interface for binding and MVVM support.
On the right side, we also have TreeviewItemViewModel class derivated into SysElementViewModel (that represents a file system element)
and becomes specialized with SysDriveViewModel, SysDirectoryViewModel and SysFileViewModel. They are used in the
USBDeviceViewModel which contains a treeview and a listview control to display the device content.

Diagram: ViewModel classes
Exchange: How to Communicate between Layers
I will try to publish a graph about the exchange later. The way is MVVM based: Views call Commands on ViewModel,
ViewModel communicate together with Mediator, ViewModel communicate with Views through Messages.
I avoid a maximum of control event handling but sometimes it is not possible or too complicated.
Localization engine
This new part is based on the very nice article from CodeProject, WPF Localization using resx - (so I am not
going to explain it...). But the solution was not satisfying me, because I don't like resx (too much work for the
developer
when it comes to update/copy/paste and rebuild new resources) and I consider C.B.R. being too small for having so many locale assemblies...There is also a
big need for an editor so that any user can manage localization or open the resource storage to database for example.
Here under are the core classes to support the localization in the XAML. I just made some small changes to existing code, like a singleton pattern
on the CultureManager or the LocalizationExtension renamed has got a ResModul property that can identify a resx file or a
xml/bin file. To complete the core model represented by the CultureManager (single public class) I have added some management
functions like GetAvailableCultures, GetAvailableModules, GetModuleResource,
SaveResources or CreateCulture: this will answer the need for an editor. Note that finally, the resx method is very restrictive and cannot cover all this overload...
I also add a CultureItem class that extend the CultureInfo. This will be mapped to a view class to fill the language gallery in the ribbon.

Diagram: Localization core classes
To extend the resx model, I choose to add a provider pattern to the CultureManager. IREsourceProvider is implemented
directly by the ResxProvider class (existing one that does not manage cultures) and FileProviderBase that derive into
BinProvider and XmlProvider (the solution I choose because file are human readable). The data model is very
simple and will be serialize or deserialized directly to the resource files. It is composed of LocalizationFile that group
all LocalizationDictionnary by culture code, each dictionary contains a collection of LocalizationItem that represent a resource.
See the programming extract to get more details about it's implementation and usage.

Diagram: Localization provider classes
The Code: Programming Extracts
In the next chapter, I would like to point out the problems that I faced and the solutions as well the nice part of code that seems interesting to share.
Conversion: Extract PDF Images
I dig into a lots of solutions - read and parse PDF, image extraction tools - before hazard brings me to a code file on the internet.
From some test file, I found that iTextSharp has a listener that you can plug onto the parser to get dedicated events and data during the parsing.
Implement a class with the IRenderListener interface and the method RenderImage will be called on each image so you can get the bytes back.
public void RenderImage(ImageRenderInfo renderInfo)
{
PdfImageObject image = renderInfo.GetImage();
PdfName filter = (PdfName)image.Get(PdfName.FILTER);
if (PdfName.DCTDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0}.jpg", _imageNames.Count));
_ImageBytes.Add(image.GetStreamBytes());
}
...
Give it to the ProcessContent method while processing PDF pages through the PdFReader and PdfReaderContentParser classes.
Note you can get text events too.
try
{
reader = new PdfReader(inputFileorFolder);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
listener = new PDFImageListener();
for (int i = 1; i <= reader.NumberOfPages; i++)
{
parser.ProcessContent(i, listener);
}
...
XPS: Fit Image on Fixed Page
How to fit images on XPS document pages? Seems like quite an easy question, but I had to test a lot before finding my Achilles heel... Do not mix pixel image size and WPF units!
Have a look at the class XPSHelper in CBR.Core. The method WriteDocument has all the mechanics to write the array
of images into a fixed document. It calls a WritePageContent method used to write the XAML page structure. Here, I specify the viewbox to fit the image
into a A4 formatted page.
private void WritePageContent(System.Xml.XmlWriter xmlWriter,
XpsResource res, double wpfWidth, double wpfHeight)
{
try
{
xmlWriter.WriteStartElement("FixedPage");
xmlWriter.WriteAttributeString("xmlns",
@"http://schemas.microsoft.com/xps/2005/06");
xmlWriter.WriteAttributeString("Width", "794");
xmlWriter.WriteAttributeString("Height", "1123");
xmlWriter.WriteAttributeString("xml:lang", "en-US");
xmlWriter.WriteStartElement("Canvas");
if (res is XpsImage)
{
xmlWriter.WriteStartElement("Path");
xmlWriter.WriteAttributeString("Data",
"M 0,0 L 794,0 794,1123 0,1123 z");
xmlWriter.WriteStartElement("Path.Fill");
xmlWriter.WriteStartElement("ImageBrush");
xmlWriter.WriteAttributeString("ImageSource",
res.Uri.ToString());
xmlWriter.WriteAttributeString
("Viewbox", string.Format("0,0,{0},{1}",
System.Convert.ToInt32(wpfWidth),
System.Convert.ToInt32(wpfHeight)));
That's why I create a WPF BitmapImage to get the image size in WPF Units...!
using (MemoryStream ms = new MemoryStream(images[i]))
{
BitmapImage myImage = new BitmapImage();
myImage.BeginInit();
myImage.CacheOption = BitmapCacheOption.OnLoad;
myImage.StreamSource = ms;
myImage.EndInit();
WritePageContent(xmlWriter, xpsImage, myImage.Width, myImage.Height);
}
Dynamic Properties
CBR has no database to store information about the books, so I had to find a way to add properties dynamically to my objects so that they can be extended.
Let's says you manage all your books by series... it is not an existing property of my Book model object. So you are not able to define the data,
nor to group or to sort them. Another requisite was that they need to be properties to work and take advantage of GroupDescriptions and SortDescription
on an ObservableCollection of book. See below in "context menu" chapter.
.NET 4 brings a fantastic but not well known feature: dynamic or Expando objects. First, add a new property to our model like below based on them.
What you will add to it will be seen like a property by WPF and reflection, but in code we generally manipulate it like a Dictionary.
private dynamic _dynamics = new ExpandoObject();
[Browsable(false)]
public dynamic Dynamics
{
get { return _dynamics; }
set { _dynamics = value; RaisePropertyChanged("Dynamics"); }
}
After defining the backstage panel that allows to manage the dynamic property reference list, I am able to use it and refresh
the Book model with a synchronized method. I am looping on both dictionaries to update the book properties.
public void SynchronizeProperties(Book bk)
{
try
{
IDictionary<string, object> dict =
(IDictionary<string, object>)bk.Dynamics;
foreach( string k in WorkspaceService.Instance.Settings.Dynamics )
{
if (!dict.Keys.Contains(k))
dict.Add(k, string.Empty);
}
foreach (string k in dict.Keys)
{
if (!WorkspaceService.Instance.Settings.Dynamics.Contains(k))
dict.Keys.Remove(k);
}
}...
Filling the backstage file information panel was hard, because WPF sees it as a dictionary when binding as a list of PropertyName/PropertyValue.
So I had to convert it to a collection of KeyValueProperty view-model class (derived from BindableObject) that has PropertyChanged event
so I can get the value back in the dynamic property.
I let you go through the code in InfoViewModel and KeyValueProperty but also in ExplorerViewModel and PropertyViewModel
which uses dynamic to fill the sort and group dropdown button menus.
Context Menu Binding
Everywhere on internet, it is pointed out that the context menu does not share the same logical tree view... and it's easy to get the parent context... But in my case,
for the context menus in the explorer, I had extra need:
- Use the commands from the
MainViewModel because commands like Read/Bookmark or Delete were already implemented
- Fill sort and group dropdown menus with properties from fixed and
dynamic and all calling the same command

Context menus: Sort and book commands
The Book Contextual Menu
As it was not easy to get up in the logical tree view to get the parent main window and to bind to these commands, I choose to implement an intermediary command in
my ExplorerViewModel that will forward it to the MainViewModel. Menu items are defined in the XAML like below:
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Read" Command="{Binding ForwardCommand}"
CommandParameter="BookReadCommand">
It was then easy to implement a command in the ExplorerViewModel that uses the Mediator to inform other views that a command occurs through
the CommandContext class that contains the CommandName as a string and the Book item as a parameter.
private ICommand forwardCommand;
public ICommand ForwardCommand
{
get
{
if (forwardCommand == null)
forwardCommand = new DelegateCommand<string>(
delegate(string param)
{
Mediator.Instance.NotifyColleagues
(ViewModelMessages.ExplorerContextCommand,
new CommandContext()
{ CommandName = param,
CommandParameter =
this.Books.CurrentItem } );
},
delegate(string param)
{
if (CatalogData != null &&
Books.CurrentItem != null)
return true;
return false;
});
return forwardCommand;
}
}
On the MainViewModel, I have a method that calls the original command by reflection each time the Mediator invokes the registered delegate.
internal void ExecuteDistantCommand(CommandContext context)
{
if (context != null)
{
new ReflectionHelper().ExecuteICommand
( this, context.CommandName, context.CommandParameter );
}
}
Sort and Group Menus
I faced the same problems with sort and group menu, plus I had to find a way to populate them with Book model class properties, some were dynamic ones.
To fill it up, I define two properties in the ExplorerViewModel like below that gives me the menu item lists. I also use reflection to get the properties
out of the book... but I quickly found that I need a view model for menu items because I need more than just a property name to continue.
public List<PropertyViewModel> SortProperties
{
get { return GetSortProperties(); }
}
PropertyViewModel below contains necessary data to fully identify the properties and a command based on the same model as in previous chapter,
that will notify the ExplorerViewModel.
public class PropertyViewModel : ViewModelBase
{
...
#region ----------------PROPERTIES----------------
private object CatalogData { get; set; }
public string Prefix { get; set; }
public string ToDisplay { get; set; }
public string Name { get; set; }
...
#region generic command
private ICommand genericCommand;
public ICommand GenericCommand
{
get
{
if (genericCommand == null)
genericCommand = new DelegateCommand<string>
(ExecCommand,
...
void ExecCommand(string param)
{
if (param.Equals("group"))
Mediator.Instance.NotifyColleagues
(ViewModelMessages.ExplorerGroup, this.Name);
else
if (param.Equals("sort"))
Mediator.Instance.NotifyColleagues(
ViewModelMessages.ExplorerSort, this.Name);
}
...
}
Note that in the ExplorerViewModel, we subscribe to both messages and after that, we can modify the SortDescription
and GroupDescription on our collection. Group command was a bit special, because it needs to forward the changes to the View to remove the GroupStyle
if needed, that's why I call the Messenger.
Mediator.Instance.Register(
(Object o) =>
{
Group( o as string );
},
ViewModelMessages.ExplorerGroup);
...
internal void Group(string propertyName)
{
PropertyViewModel prop = GetGroupProperties().Find(p => p.Name == propertyName);
IEnumerable<PropertyGroupDescription> result =
Books.GroupDescriptions.Cast<PropertyGroupDescription>().
Where(p => p.PropertyName == prop.Prefix + prop.Name);
if (result != null && result.Count() == 1)
{
Books.GroupDescriptions.Remove(result.First());
}
else
{
Books.GroupDescriptions.Add(new PropertyGroupDescription
(prop.Prefix + prop.Name));
}
Messenger.Default.Send<MessageBase>( new MessageBase(this) );
RaisePropertyChanged("Books");
}
In the XAML, mainly have to define the right template for menu items with the display and the command bindings.
<!---->
<Style x:Key="PropertyViewModel">
<Setter Property="MenuItem.Header" Value="{Binding ToDisplay}"/>
<Setter Property="MenuItem.Command" Value="{Binding GenericCommand}" />
<Setter Property="MenuItem.IsCheckable" Value="true" />
</Style>
<Style x:Key="GroupMenuItemStyle" BasedOn="{StaticResource PropertyViewModel}" >
<Setter Property="MenuItem.CommandParameter" Value="group" />
</Style>
<Style x:Key="SortMenuItemStyle" BasedOn="{StaticResource PropertyViewModel}" >
<Setter Property="MenuItem.CommandParameter" Value="sort" />
</Style>
Implement Drag and Drop
On the application, from outside
You can drop a comic file or library to the application to open it directly. Easy to implement, add the drop event handler, check if we support the file extension
and execute open command in the MainViewModel. Do not forget to allow drop on you main window through the AllowDrop property.
private void RibbonWindow_Drop(object sender, DragEventArgs e)
{
try
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
MainViewModel mvm = DataContext as MainViewModel;
string[] droppedFilePaths =
e.Data.GetData(DataFormats.FileDrop, true) as string[];
if (FileService.Instance.FindCatalogFilterByExt
(System.IO.Path.GetExtension(droppedFilePaths[0])) != null)
mvm.CatalogOpenFileCommand.Execute
(droppedFilePaths[0]);
else
if (FileService.Instance.FindBookFilterByExt
(System.IO.Path.GetExtension(droppedFilePaths[0])) != null)
mvm.BookOpenFileCommand.Execute(droppedFilePaths[0]);
}
}
catch (Exception err)
{
ExceptionHelper.Manage("MainView:RibbonWindow_Drop", err);
}
}
Explorer and Device View
When drag & drop comes from the internal controls, so we have to manage MouseDown, MouseMove events and initiate a drag & drop operation
on each possible source control. To make it easier, I develop a DragHelper that you plug on each control that needs to be a source - so you don't care anymore
about mouse handling and item preview. The drop destination needs to be managed as usual depending your needs. The DragHelper attaches itself
to the control given in the constructor.
public DragHelper( Control attachedView )
{
_Attached = attachedView;
_Attached.PreviewMouseLeftButtonDown +=
new MouseButtonEventHandler(PreviewMouseLeftButtonDown);
_Attached.PreviewMouseMove += new MouseEventHandler(PreviewMouseMove);
}
void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_startPoint = e.GetPosition(null);
}
void PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && !IsDragging)
{
Point position = e.GetPosition(null);
if (Math.Abs(position.X - _startPoint.X) >
SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(position.Y - _startPoint.Y) >
SystemParameters.MinimumVerticalDragDistance)
{
Mouse.Capture(Application.Current.MainWindow);
StartDrag(e);
}
}
}
If we meet the drag distances, we capture the mouse in the application, and send a StartDragEvent to the attached control - so it can prepare data and callback
DoDragDrop below. Shortly, it allows drop, attaches itself to the main window DragOver event, creates an adorner that contains the source element
preview, and then calls the real DoDragDrop function. When it is released, we clean up all.
public void DoDragDrop( DataObject data, UIElement source )
{
DragScope = Application.Current.MainWindow.Content as FrameworkElement;
bool previousDrop = DragScope.AllowDrop;
DragScope.AllowDrop = true;
DragEventHandler draghandler = new DragEventHandler(ScopeDragOver);
DragScope.PreviewDragOver += draghandler;
_DragAdorner = new DragAdorner(DragScope, source, true, 0.5);
AdornerLayer layer = AdornerLayer.GetAdornerLayer(DragScope as Visual);
layer.Add(_DragAdorner);
DragDropEffects de = DragDrop.DoDragDrop(_Attached, data, DragDropEffects.Move);
Mouse.Capture(null);
DragScope.AllowDrop = previousDrop;
AdornerLayer.GetAdornerLayer(DragScope).Remove(_DragAdorner);
_DragAdorner = null;
DragScope.PreviewDragOver -= draghandler;
IsDragging = false;
}
How to Use It
To use it is very easy, just instantiate a DragHelper on a control like below:
_drager = new DragHelper(CatalogListBox);
_drager.OnStartDrag += new StartDragEventHandler(drag_OnStartDrag);
Then, create the StartDragEventHandler that will be called by the helper when a drag & drop operation is validated.
Find your control item and create the data you need to manage the drop. Call the DragHelper.DoDragDrop function to validate and continue the operation.
void drag_OnStartDrag(object sender, MouseEventArgs e)
{
ListBoxItem item = VisualHelper.FindAnchestor<ListBoxItem>
((DependencyObject)e.OriginalSource);
if (item != null)
{
Book bk = (Book)CatalogListBox.ItemContainerGenerator.
ItemFromContainer(item);
DataObject dragData = new DataObject("CBR.Book.Path", bk.FilePath);
_drager.DoDragDrop(dragData, item);
}
}
In any destination control, handle the _DragOver, _DragLeave and _Drop handler as needed. Check whether the data comes
from you, then execute any business code like below to make a file copy.
private void listViewContent_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("CBR.Book.Path"))
{
string path = e.Data.GetData("CBR.Book.Path") as string;
string destFile = Path.Combine(
(this.FolderTree.SelectedItem as SysElementViewModel).FullPath,
Path.GetFileName(path) );
FileService.Instance.CopyToDevice(path, destFile,
this.cbDiskType.SelectedItem as DeviceInfo);
}
}
Implement USB and Device Detection
WMI Event Watcher
WMI as Windows Management Instrumentation is a nice (but complicated feature) that I discover when a friend of mine says "what about dragging my book directly
to my reader ?"...ha ha so simple it is? and what about e-book reader supported formats? This drives me to two new features: DeviceConfigView in the backstage
that holds devices/manufacturers and supported formats as a referential, and the new USBDeviceView to display USB drives that can be associated to devices.
The idea comes to simply detect USB peripherals for most e-book reader (and later 7 phones). After hours of internet digging and test, no way to get it straight because WMI
provides a lot of information but none in the same structure. Here is the solution I implement by writing a helper class that first search for existing devices at the connection
time and then watch over usb events.
WMI provides helpful classes and support a query language called WQL: ObjectQuery - to define a query, ManagementObjectSearcher - like an Execute
and return collection of ManagementBaseObject - the result that contains asked information.
First, I need two ManagementEventWatcher to get WMI events onto the Win32_USBControllerDevice structure for __InstanceCreationEvent
and __InstanceDeletionEvent. Note that the WITHIN clause is like a timer and allow pooling events.
addedWatcher = new ManagementEventWatcher
("SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance
ISA \"Win32_USBControllerDevice\"");
addedWatcher.EventArrived += new EventArrivedEventHandler(HandleAddedEvent);
addedWatcher.Start();
...
if (addedWatcher != null)
addedWatcher.Stop();
Handling the creation event is a bit complicated, because a lot of things are created when plugging a device. First we get that:
Antecedent: \\FR-L25676\root\cimv2:Win32_USBController.DeviceID=
"PCI\\VEN_8086&DEV_293C&SUBSYS_00011179&REV_03\\3&21436425&0&D7"
Dependent: \\FR-L25676\root\cimv2:Win32_PnPEntity.DeviceID="USBSTOR\\
DISK&VEN_USB&PROD_FLASH_DISK&REV_1100\\AA04012700076941&0"
I extract from that two pieces of information that allow me to query the Win32_PnPEntity:
- the PNP Device ID that is the
Win32_PnPEntity key => USBSTOR\\DISK&VEN_USB&PROD_FLASH_DISK&REV_1100\\AA04012700076941&0
- the Device ID => AA04012700076941&0
and the result is a multirow result set.
I only create an USB drive if "USBSTOR" is founded. Then I parse the line with the service = "disk" to get more information and
call GetDiskInformation that is the same as GetExistingDevices.
ManagementBaseObject targetInstance =
e.NewEvent["TargetInstance"] as ManagementBaseObject;
DebugPrint(targetInstance);
string PNP_deviceID = Convert.ToString(targetInstance["Dependent"]).
Split('=').Last().Replace("\"", "").Replace("\\", "");
string device_name = Convert.ToString(targetInstance["Dependent"]).
Split('\\').Last().Replace("\"", "");
ObjectQuery query = new ObjectQuery(string.Format("Select *
from Win32_PnPEntity Where DeviceID like \"%{0}%\"", device_name));
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
ManagementObjectCollection entities = searcher.Get();
foreach (var entity in entities)
{
string service = Convert.ToString(entity["Service"]);
if (service == "USBSTOR")
device = new USBDiskInfo();
}
if (device != null)
{
foreach (var entity in entities)
{
string service = Convert.ToString(entity["Service"]);
if (service == "disk")
{
GetDiskInformation(device, device_name);
Devices.Add(device);
if (EventArrived != null)
EventArrived(this, new WMIEventArgs()
{ Disk = device, EventType = WMIActions.Added });
....
To handle the device removal, I implemented this method: get the instance fired by event, extract the PNP device id and if we remove the device from our collection,
fire an internal event targeting the application.
private void HandleRemovedEvent(object sender, EventArrivedEventArgs e)
{
try
{
ManagementBaseObject targetInstance =
e.NewEvent["TargetInstance"] as ManagementBaseObject;
DebugPrint(targetInstance);
string PNP_deviceID = Convert.ToString(targetInstance
["Dependent"]).Split('=').Last().Replace
("\"", "").Replace("\\", "");
USBDiskInfo device = Devices.Find(x => x.PNPDeviceID == PNP_deviceID);
if( device != null )
{
Devices.Remove( device );
if (EventArrived != null)
EventArrived(this, new WMIEventArgs()
{ Disk = device, EventType = WMIActions.Removed });
}
}
....
For existing devices, I go from Win32_DiskDrive through Win32_DiskDriveToDiskPartition through Win32_LogicalDiskToPartition
to reach Win32_LogicalDisk. This makes a cascade of query and loop like below:
ObjectQuery diskQuery = new ObjectQuery
("Select * from Win32_DiskDrive where InterfaceType='USB'");
foreach (ManagementObject drive in new ManagementObjectSearcher(diskQuery).Get())
{
ObjectQuery partQuery = new ObjectQuery(
String.Format("associators of {{Win32_DiskDrive.DeviceID='{0}'}}
where AssocClass = Win32_DiskDriveToDiskPartition", drive["DeviceID"])
);
DebugPrint(drive);
foreach (ManagementObject partition in new ManagementObjectSearcher
(partQuery).Get())
{
ObjectQuery logicalQuery = new ObjectQuery(
String.Format("associators of {{Win32_DiskPartition.DeviceID='{0}'}}
where AssocClass = Win32_LogicalDiskToPartition", partition["DeviceID"])
);
DebugPrint(partition);
foreach (ManagementObject logical in new ManagementObjectSearcher
(logicalQuery).Get())
{
DebugPrint(logical);
USBDiskInfo disk = new USBDiskInfo();
ParseDiskDriveInfo(disk, drive);
ParseDiskLogicalInfo(disk, logical);
devices.Add(disk);
}
}
}
Use it in the Application
First, create an instance of the watcher in the MainWindowView, start it and get events with a new handler. Do not forget to manage existing devices
that were plugged before the application start.
private WMIEventWatcher wmi = new WMIEventWatcher();
private void RibbonWindow_Loaded(object sender, RoutedEventArgs e)
{
wmi.StartWatchUSB();
wmi.EventArrived += new WMIEventArrived(wmi_EventArrived);
MainViewModel mvm = DataContext as MainViewModel;
if (mvm != null)
{
foreach (USBDiskInfo disk in wmi.Devices)
mvm.SysDeviceAddCommand.Execute(disk);
}
}
In the event handler, as it is called on another thread, we use the application dispatcher to call the commands to add or remove a device. This will update/create
the device view. Do not forget to close the watcher in the OnClose event of the main window.
void wmi_EventArrived(object sender, WMIEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, (ThreadStart)delegate {
MainViewModel mvm = DataContext as MainViewModel;
if (mvm != null)
{
if (e.EventType == WMIActions.Added)
mvm.SysDeviceAddCommand.Execute(e.Disk);
else
if (e.EventType == WMIActions.Removed)
mvm.SysDeviceRemoveCommand.Execute(e.Disk);
}
});
}
Localization engine
How it works?
To complete the core model explanation, everything happens in the GetValue method of the LocalizationExtension. I extend
this existing method to ask the configured provider. ConvertValue is the same, GetObject has been overridden in all
provider to search the asked key. More changes occurs in the GetDefaultValue, because the provider will create the file, dictionary and resource item.
protected override object GetValue()
{
if (string.IsNullOrEmpty(Key))
throw new ArgumentException("Key cannot be null");
object result = null;
IResourceProvider provider = null;
try
{
object resource = null;
if (GetResource != null)
resource = GetResource(ResModul, Key, CultureManager.Instance.UICulture);
if (resource == null)
{
if (provider == null)
provider = CultureManager.Instance.Provider;
if (provider != null)
resource = provider.GetObject(this, CultureManager.Instance.UICulture);
}
result = provider.ConvertValue(this, resource);
}
catch (Exception err)
{
}
try
{
if (result == null)
result = provider.GetDefaultValue(this, CultureManager.Instance.UICulture);
}
catch (Exception err)
{
}
return result;
}
Use it in the application
Configure the provider you want through settings LocalizeProvider and LocalizeFolder, then just replace the actual content with something like
below: ResModul is the destination file, Key is the unique resource identifier in the file and DefaultValue is the displayed text.
There is no need to add an extra namespace because the resource extension is associated with standard XAML namespace.
Text="{LocalizationExtension ResModul=CBR.Backstage, Key=ConvertView.Title, DefaultValue=Convert}"
Note that a localize version will be created and displayed in the designer and the program until you work on it. This helps identifying
quickly what you have missed. At the runtime, all resources for the running culture a going to be added automatically to the files, no need to create one!

Implement language choice and editor
I design a dropdown button associated with a Gallery that display the CultureItem provided by the GetAvailableCulture method of
CultureManager. They are mapped to a LanguageMenuItemViewModel which derive from MenuItemViewModel
that is the generic class for all CBR menus items in the MVVM pattern.

The associated XAML below define it:
<Fluent:Gallery ItemsSource="{Binding Languages}"
GroupBy="Tag" x:Name="languageGallery" MinItemsInRow="1" MaxItemsInRow="2"
Orientation="Horizontal" SelectionChanged="languageGallery_SelectionChanged">
<Fluent:Gallery.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Icon}" Width="16" Height="16" />
<TextBlock Text="{Binding ToDisplay}" />
</StackPanel>
</DataTemplate>
</Fluent:Gallery.ItemTemplate>
<Fluent:Gallery.ItemContainerStyle>
<Style TargetType="{x:Type Fluent:GalleryItem}"
BasedOn="{StaticResource {x:Type Fluent:GalleryItem}}">
<Setter Property="Fluent:GalleryItem.IsSelected" Value="{Binding IsChecked}"/>
</Style>
</Fluent:Gallery.ItemContainerStyle>
</Fluent:Gallery>
The editor is a very simple tool window. In the source pane, you choose the language and module you want to work on. Click the Select button to use
the language in the application. Note that the window is amodal, so you can continue to use C.B.R. to check your work. The grid display the selected
resources: Key, Default is the original text from development, Translated is the one you need to work on and that will be displayed.
In the New pane, you can select a culture that does not exist actually, add an icon file name and then click Create button to add a new language in
"memory". Use the Save button to commit your changes to the files.

Extend the engine
If you want to extend it with a database provider for example, write your model and provider and feel free to provide us feedback ! I need it for another project :-)
Note that CBR is not fully localized on beta items and tooltips...
3D and 2 pages flip viewer
For a long time, I had a look to Mistu Futura "2 pages book", but I was
not sure about the performance of an ItemControl binded to more than 50
bitmap pages...and recently I discover after short tests that binding only
occurs when turning the pages! So I decided to go further and adapt this
control to my needs.
Original code is not very well documented and
content is based on "full fit" and xaml user controls, so no scaling
troubles...so I had to found a solution where bitmaps allways fit the pages
the best they can. I also need it to be in a scrollview and answers my
actual book commands.
How it works ?
First, take all original code into a folder, change the class names to be
more clear, group, and transform the user control into a control. This lead us
to LinearGradiantHelper, PageParameters,
CornerOrigin and PageStatus (no changes) - and
TwoPageBook (the new main control) with TripleSheet control
that represent the 3 page sides on the right or left part of the book. When
animating a page we can see the side in front of us, the one behind (when it
rolls the page) and the front side on the page before (or after).
I modify the control to have his default style and wrap it into a
scrollviewer like the template below. I remove a few things I do not need
like zoom on current page...
<Style x:Key="{x:Type local:TwoPageBook}" TargetType="{x:Type local:TwoPageBook}">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TwoPageBook}">
<ScrollViewer Name="PART_ScrollViewer" Focusable="True"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid Name="PART_Content">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="50*" />
</Grid.ColumnDefinitions>
<local:TripleSheet Grid.Column="0" x:Name="sheet0" HorizontalAlignment="Right"
IsTopRightCornerEnabled="false" IsBottomRightCornerEnabled="false" />
<local:TripleSheet Grid.Column="1" x:Name="sheet1" HorizontalAlignment="Left"
IsTopLeftCornerEnabled="false" IsBottomLeftCornerEnabled="false" />
</Grid>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then, I complete the class OnApplyTemplate method to grap my
parts to be sure _Content will be scaled and the
_ScrollContainer will be used later to be able to fit into the document view.
I also add Scale, FitMode and a
CurrentPageIndex because original code manage a sheet index (index
divided by 2). Then I change the TripleSheet template to have
the content to fit and colors to fill the gap when bitmap cannot shrink
correctly. Note that the Fit method make an entorse to the
genericity of the control by using the Page model class...that
was the only way to find the content size ! -:(
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_ScrollContainer = (ScrollViewer)GetTemplateChild("PART_ScrollViewer");
_Content = (FrameworkElement)GetTemplateChild("PART_Content");
_scaleTransform.CenterX = 0.5;
_scaleTransform.CenterY = 0.5;
_Content.LayoutTransform = _scaleTransform;
[...]
private void Fit()
{
if (FitMode == DisplayFitMode.Height)
{
Scale = (this._ScrollContainer.ViewportHeight - FIT_BORDER) / (Items[CurrentSheetIndex] as CBR.Core.Models.Page).Image.Height;
}
else if (FitMode == DisplayFitMode.Width)
{
Scale = (this._ScrollContainer.ViewportWidth - FIT_BORDER) / ((Items[CurrentSheetIndex] as CBR.Core.Models.Page).Image.Width * 2);
}
}
For performance reasons, I split my book view into two separate classes :
simple page view and 2 pages view. OOppps ! I have to manage the swap
between the 2 modes and passing parameters ! As the book is loaded, I just
need the current page index. The book views communicate through the main
view that receive a SwapTwoPageView message and convert the
view into the desired one like illustrated below.
Mediator.Instance.RegisterHandler<BookViewModelBase>(ViewModelMessages.SwapTwoPageView,
(BookViewModelBase o) =>
{
SwapTwoPageMode(o);
});
[...]
internal void SwapTwoPageMode(BookViewModelBase o)
{
ViewModels.Remove(o);
BookViewModelBase newModel = null;
BookViewModelBase oldModel = null;
if (o is ComicViewModel)
{
ComicViewModel comic = o as ComicViewModel;
newModel = new TwoPageViewModel(o.Data, comic.CurrentPage.Index, comic.FitMode, comic.PreviousScale);
}
else
{
TwoPageViewModel comic = o as TwoPageViewModel;
newModel = new ComicViewModel(o.Data, comic.CurrentPageIndex, comic.FitMode, comic.PreviousScale);
}
oldModel = o;
ViewModels.Add(newModel);
SetActiveView(newModel);
ViewModels.Remove(oldModel);
}
Note that many of the books I found are not really compliant with this
view because of page ratio that are never the same or contains double scan.
Don't be surprised, but as I do not process the images and read them in
memory, I can't make any adjustments.
Contributors
Many thanks to SevenZip (which allows me to uncompress in memory), Fluent Ribbon
for this excellent control library and also for all the internet contributors I read along this project.
Your contribution
As the last version was pretty long to come out and was downloaded round about 10 000, I think you are perhaps interested to be able to give some
more precise feed back, I put all planned features in the "Issue tracker" page of the project.
So now you are able to give me feedback about your needs priority by voting or create new items. This will help me to plan next version content.
Conclusion
I hope you will enjoy it as much as I do and that you will find your needs in my code or in my software. If you get any better solution for the problems I faced, please forward...
History
- v0.1
- First version: basically the same as old project
- v0.2
- Complete refactoring of book service and view model to extend to more document type
- PDF conversion to image files, zip and XPS (bug resolved + thumbnail) is working
- Add the dynamic frames, editor, load/save
- Add 7 Phone simulator (have to work on it)
- Manage XPS documents in the library, Add XPS viewer
- Add the document type icon in the explorer
- v0.3
- Add magnifier size and scale
- New file info view in the backstage
- Add dynamic properties on book and settings
- Sorting and grouping in the explorer with new design
- Rework on conversion: Images, PDF, Cbr/rar, Cbz/zip, XPS to the destination formats Images, CBZ and XPS
- Suppress
MainViewModel and ExplorerViewModel dependencies
- Add view notifications and Messages from MVVM Light for
ViewModel=>View notifications
- Make thread better on open catalog, no more IHM freeze, less than 10s for 400 books
- Avoid recreating cover file if exists
- Rework on rating control and splitter
- v0.4
- Added/renew images, Add icons in context menu
- Finish the page navigation commands on comics
- Rework on device and USB ribbon, moved the combo, organize it
- Highlight on item view is working with drag and drop
- Book double-click in explorer to open
- Ribbon tabs 'book' and 'device' becomes contextual ('book' visible only when a book is open, 'device' only when something is detected)
- Key management while reading : arrows and page up/down (was lost in rewriting)
- New command : Remove book from library
- ePUB format support (file and memory, icon, cover preview, etc...) and a new document view with Toc explorer and WebBrowser
- Usb event for devices detection and new USB device view in the ribbon device group with drag & drop support
- Device config in backstage to list supported devices, this will help when transferring data to a device to know about the needed transformation
- v0.5
- New explorer based on listview (previous listbox with wrap panel loose virtualization) with three views: grid, simple thumb, extended
- New "home" page with actions and headlines feed (xml from web)
- New extended options dialog (not enough place in backstage panel), Registering CBR with file types and move dynamic properties
- Add "file open" from explorer and shell registering
- Add a help button that display an online page
- Add localization (FR+ENG) and management
- Add dropdown button to choose in available languages and starting language option
- Add a localization editor to manage languages
- Add an option "start with" language
- Make xml and binary localization providers
- Move new search box to explorer and device info view
- Disable sort/group/search in explorer if no books
- Rework style in search box control
- Create white shadowed button style to apply on backstage buttons like office
- Bug: New catalog was not added in the recent file list after close/save
- Bug: Replace new WeakAction and Mediator as Sacha Barber points out the not-garbaged references (thanks -:))
- v0.6 & 0.55
- 20 Issue trackers are closed and a lot of bugs too
- Localize view is now MVVM and delete is working. Added the unused
flag (take care that it goes to true only when displaying screen
elements)
- Backstage - new input/output format choice control for the
conversion
- Backstage - Add display, behaviour and register file type options in
the extended options dialog
- Explorer list view has been transformed to a custom control. New
group header, colunms order and size are saved
- Single instance and file argument management
- New optimized full screen display options and a flying zoom control
- 3D flip and two page book view (adapted from Mitsu Futura original code)