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 other ebook
formats. 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 :
OPDS feed,
function to localize by code, multi-document
and toolboxes in MVVM design,
re-designed frame editor in mvvm way for dynamic books, new tablet and 7 Phone
simulators, 'Any CPU' and compatible with 32 & 64 bit plateforms,
Image merger for PDF conversion, etc...
- 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" feeds
- 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, ZoomFlyer,
Extended ListView
- XPS reading and writing, ePUB parsing and conversion
- Single instance, parameter processing and file type registering
Chapters
Screenshots
You can find more screenshots on the product web site :
http://guillaume.waser.free.fr
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.
Update... !
Fileservice
and FileInfo
classes has been
replaced by DocumentFactory
, DocumentInfo
and
DocumentType
that link to the Core model.
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.
Update... !
ExtendedInfo
contains the data for the extended
options. FeedInfo
and FeedItemInfo
the data for
the backstage view FeedInfoView
. ProxyInfo
class
has been added to manage any special internet settings.
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.
Update... !
The model has been completed with new classes like FeedView/Model
to display OPDS feeds....
The main changes are due to the AvalonDock integration that brings
PaneViewModel
derivated into ToolViewModel
(that represents
documents and toolbox view models).
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
set.
See the programming extract to get more details about it's implementation and usage.
Diagram: Localization provider classes
ePUB and OPDS models
I had a complete rework on the previous model (that cannot extend to
conversion or writing functionnalities). ePubManager
is the
parsing class and below are the model classes. This one is much better, even
if ePUB has got a lot of specification versions...it must fit for nearly all
reading. Note that the viewer is based on IE (need to register some key for
better emulation mode - done by the setup - you will find them in the
solution). In case of bad encoding detection, you can use the view context
menu.
OPDS is a special feed format dedicated to electronical publication. CBR
include a feed management view and a browser view that allow to navigate the
Rss and download books. Below is the model that suit to my needs (not the
OPDS specification) and the OpdsManager
is the main class that
parse the feed to create the objects for View and ViewModels
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.
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);
}
...
Update... !
The RenderImage
method has been improved with this new code. Before /flat
and /lzw were not processed and GetImageAsBytes
works better for image type
that can be read by ITextSharp
.
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:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_JPG));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.JPXDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_JP2));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.LZWDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_TIF));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.FLATEDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_PNG ));
_ImageBytes.Add(image.GetImageAsBytes());
}
...
New: Merge images by page (for PDF)
I have noticed in the past with some tests that PDF image extraction
sometimes results in hundred of images because each page is cutted in
several parts. I don't know where it comes from, but the solution is there. I
have added a checkbox option in the conversion panel to merge images in case
of a number difference with the page count.
As my conversion engine is based on a reader (or extractor), a pipe with
the image bytes, then a writer to finalize the choosen destination format, i
put in the middle a ImageJoiner
class that combine the original
bytes into real pages.
I first modify the ImageListener
to be sure that page index
is allways included in the image file name. When I detect a difference in
the Read
method of PDFImageReader
class, I give
the array to the ImageJoiner
class and take his array as the
result
if (settings.CheckResult && reader.NumberOfPages != listener.ImageNames.Count)
{
if (settings.JoinImages)
{
progress(string.Format("Extracting {0} : {1} images for {2} pages - Try to merge !", inputFileorFolder, listener.ImageNames.Count, reader.NumberOfPages));
ImageJoiner cp = new ImageJoiner();
cp.Merge(listener.ImageBytes, listener.ImageNames);
progress(string.Format("Merge to {0} new images...", cp.NewImageNames.Count));
imageBytes.AddRange(cp.NewImageBytes);
imageNames.AddRange(cp.NewImageNames);
The ImageJoiner
is quite simple and the trick is in the
second Merge
method : it takes indexes to process as
parameters. I first create a list of the corresponding BitmapImage
and compute the size of the destination bitmap. Then create a
RenderTargetBitmap
that match the destination. With a
DrawingVisual
, I open is context and draw all the bitmap in it. Then
I ask the
RenderTargetBitmap
to draw the visual and after that extract
the bytes to the new array...here it is !
public void MergeGroup(List<byte[]> imageBytes, List<string> imageNames, int start, int end, int index)
{
try
{
List<BitmapImage> bmps = new List<BitmapImage>();
double maxWidth = 0, maxHeight = 0, position = 0;
for (int i = start; i <= end; i++)
{
MemoryStream ms = new MemoryStream(imageBytes[i]);
BitmapImage myImage = new BitmapImage();
myImage.BeginInit();
myImage.StreamSource = ms;
myImage.CacheOption = BitmapCacheOption.None;
myImage.EndInit();
bmps.Add(myImage);
maxWidth = Math.Max(myImage.Width, maxWidth);
maxHeight += myImage.Height;
}
RenderTargetBitmap temp = new RenderTargetBitmap((int)maxWidth, (int)maxHeight, 96d, 96d, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using (DrawingContext ctx = dv.RenderOpen())
{
foreach (BitmapImage bi in bmps)
{
ctx.DrawImage(bi, new System.Windows.Rect(0, position, bi.Width, bi.Height));
position += bi.Height;
}
ctx.Close();
}
temp.Render(dv);
NewImageNames.Add(string.Format("{0:0000}.jpg", index));
NewImageBytes.Add(StreamToImage.BufferFromImage(temp));
bmps.Clear();
...
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>
Update... !
Based on the same principe, i have implemented an automatic property
discovering based on an Attribute
class that allow me to define
which property can be sorted, grouped along with the localize code...below
is the schema : The PropertyHelper
class provides me directly
with the PropertyViewModel
collection needed to fill up
the menus.
I extend my book model like below to define that this property is not
groupable, but sortable and that the localize key is
Core.Properties.FilePath. Advantage is that my menus are now automatically
updated if i changes properties or the language !
[UserPropertyAttribute(false, true, "Core.Properties.FilePath")]
public string FilePath
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 CultureInfoItem
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.
Update 1... !
This bloomy engine was not thinked for localizing from code ! Before the
tabs for multi-document support, i had no resource out of XAML. But when it
comes to messages or tab titles like "home" and "devices"...MarkupExtension
fails because it is for XAML only ! So i found a
turn arround solution. For messages to be displayed in message boxes, just
ask the resource. Tabs need to be updated via the ViewModel when the
language is changed.
I wrote a new function in CultureManager
to ask a
localization based on the same principle...GetLocalization
(string
modul, string key, string defaultValue) will go through the provider to find
if an existing LocalizationExtension
or LocalizationItem
exist. If not it will manage everything like the original engine do.
To put it in a ViewModel
like my "Home" tab, I add to
subscribe to the CultureManager
event when culture change...and
unsubscribe in the Dispose
method. That it's....hours and hours
to find an elegant solution !
public HomeViewModel()
{
CultureManager.Instance.UICultureChanged += new CultureEventArrived(Instance_UICultureChanged);
DisplayName = CultureManager.Instance.GetLocalization("ByCode", "DocumentTitle.Home", "Home");
}
void Instance_UICultureChanged(object sender, CultureEventArgs e)
{
DisplayName = CultureManager.Instance.GetLocalization("ByCode", "DocumentTitle.Home", "Home");
}
protected override void OnDispose()
{
base.OnDispose();
CultureManager.Instance.UICultureChanged -= new CultureEventArrived(Instance_UICultureChanged);
}
Update 2... !
Cultures are now identified by the IETF code from the net class
CultureInfo
. It combines language and country code like fr-FR :
french in FRANCE. fr-CA is french in CANADA. CultureItem
class has been removed and the icon is now named based on IETF code too.
Update 3... !
The localize editor has been improved and display now an additionnal
column about unused resources...Then it has been completed with WPF
spellcheck. Note that you will have to install net language packs ! and by
the way, fluent gallery are now displaying correctly with the manage button
at the bottom.
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 " src="http://www.codeproject.com/script/Forums/Images/smiley_smile.gif" />
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 ! - src="http://www.codeproject.com/script/Forums/Images/smiley_frown.gif" /> /p>
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 feedback
As the last version was pretty long to come out and 0.6 was downloaded around 60 000
times, 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.
Deliverables - Installation notes
With version 0.7 and installer, always remove the previous version or some files are not going to be replaced and big troubles will occurs. I will work on a better solution.
There is no more download on CodeProject, because there 4 deliverables of consequent sizes : 9 Mo for installers, 16 Mo of sources and 7Mo of direct binaries. Please visit the link to get the needed one.
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 (simplified...)
- v0.7
- Ribbon tabs and commands have been re-organized
- ePUB : Complete refactoring
- Add a new dedicated feed viewer for opds stream
- Localize Engine improved: add functions to manage resource by code,
now based on ietf language/country code, remove image from xml (based on
code), remove CultureItem class. Better copy, management view and
gallery, spell check
- Dynamic Books: Designer for frames is now MVVM and much better. It
is based in ItemControl and templated with a Canvas. Also added 7Phone
and Tablet simulators
- Release mode are now "Any CPU" to be compliant with the
running plateform. Added 32 and 64 bits versions of 7z.dll. Installer
for x32 and x64, registry keys for ie emulation mode for epub/html view
- Multi-Doc: C.B.R. has now an option for multi document display with
tabs - Integrate customized AvalonDock 2 library.
- PDF image extraction improved and new image merging conversion option
- New BrowseForControl
- Customized xps viewer to suppress toolbars and bind it to cbr
commands
- Add quick start manual and button in the home page, manage internal
CBR feed language