WPF, ListView d’images, MVVM et Drag and Drop… – Fin

E-mail Print PDF
User Rating: / 80
PoorBest 
There are no translations available.

Introduction

Les projets précédents ont été redécoupés afin de fournir 2 assemblies directement utilisable.

Pour ceux qui souhaitent ajouter rapidement un treeview d’images avec une gestion de Drag&Drop (Voir les 4 précédents chapitres ) voici les étapes de réalisation.

L’exemple suivant montre comment réaliser une application respectant l’architecture M-V-VM avec les briques Drag and Drop et Listview d’images

Application M-V-VM exemple avec Liste en arbre de type TreeView

Il vous faudra :

  1. Ajouter les assemblies
  2. Définir un objet possédant l’Interface IImageFile (et IParentItem si vous souhaiter des structures hiérarchiques)
  3. Définir un objet ViewModel dérivant de CustomImageFileViewModel si vous voulez gérer le DragAndDrop provenant d’applications externes
  4. Initialiser les images dans CollectionImageViewModel

Implémentation

Ajouts des assemblies

La première étape est de télécharger ou compiler les assemblies suivantes (à la fin de l’article) :

  • ImageFileListView : Contient la partie visuelle, le View Model et les interfaces du modèle à implémenter
  • DragDropManager : Contient les mécanismes de Drag and Drop qui pourront être appliqués sur la lise

Ajout de la liste dans la View

Pour ajouter la liste dans la View vous devez définir une référence dans le XAML à l’assemblie ImageFileListView.

xmlns:m="clr-namespace:ImageFileListView.View;assembly=ImageFileListView" 

 

 

Dans le code XAML ajouter le composant de la façon suivante

 <m:ImageFileListView x:Name="listView1" ClipToBounds="False" Grid.ColumnSpan="2" />

Implémentation de la couche Model

Les objets utilisés doivent implémenter l’interface IImageFile. Si vous souhaitez gérer une hiérarchie (par exemple des répertoires) vous devez en plus utiliser l’interface IParentItem.

Voici un exemple d’implémentation (disponible dans l’exemple).

Exemple d’implémentation pour gérer des fichiers  :

 

public class ImageFile: IImageFile
{
public string FileName { get; set; }
public IParentItem Parent { get; set; }

public virtual bool IsAvailable
{
get
{
return File.Exists(FileName);
}
}

public virtual Stream LoadImage()
{
byte[] buffer = File.ReadAllBytes(FileName);
return new MemoryStream(buffer);
}

#region ICloneable Membres

public virtual object Clone()
{
ImageFile o
= new ImageFile();
o.FileName
= this.FileName;
return o;
}

#endregion
}

 

Exemple d’implémentation pour gérer les répertoires d’un disque

 

public class ParentImageFile : ImageFile, IImageFile, IParentItem
{
public override bool IsAvailable
{
get
{
return Directory.Exists(FileName);
}
}

public override Stream LoadImage()
{
return null;
}

public List<IImageFile> ImageFiles { get; set; }

public override object Clone()
{
ImageFile clonedItem
= null;
ParentImageFile o
= new ParentImageFile();
o.FileName
= this.FileName;
o.ImageFiles
= new List<IImageFile>();
foreach (IImageFile item in this.ImageFiles)
{
clonedItem
= item.Clone() as ImageFile;
clonedItem.Parent
= o;
o.ImageFiles.Add(clonedItem);
}
return o;
}
}

 

Implémentation de la couche View-Model

Vous pouvez directement utiliser l’héritage d’un objet CustomImageFileViewModel.

Le lien entre la couche Model et la couche View-Model est réalisée par le code suivant :

namespace WpfListViewMVVMSample.ViewModel
{
public class ImageFileViewModel: CustomImageFileViewModel
{
public override void CreateFromFileName(string imageFileName)
{
ImageFile
= new ImageFile() { FileName = imageFileName };
}
}
}

Initialisation des comportements DragAndDrop dans la vue

Initialisation

Dans l’initialisation de la fenêtre, sur la partie View  :

 

namespace WpfListViewMVVMSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{

public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
DragDropManagerListBoxAdornerViewModel
<ImageFileViewModel> dragDropManager
= new DragDropManagerListBoxAdornerViewModel<ImageFileViewModel>
(listViewImages.ImageFilesListBox);
dragDropManager.RegisterDragDropManagerOperation(
new DragDropManagerOperationTreeView<ImageFileViewModel>());

}
}
}

 

  • La vue initialise le ViewModel : (this.DataContext = new MainWindowViewModel)
  • La vue initialise le DragDropManager avec l’objet ViewModel (ImageFileViewModel) décrit plus haut et indique que les actions de Glisser-Déplacer interviendront sur la liste listViewImages
  • Le dragdrop est initialisé pour pouvoir gérer une structure d’arbre récursive.

Initialisation du ViewModel de la liste

Le viewModel dérive naturellement de INotifyPropertyChanged et l’on retrouve classiquement PropertyChangedEventHandler et RaisePropertyChanged.

Pour notre interface, nous avons 3 propriétés :

  • ImagesPath : La donnée représentant de la zone de texte du répertoire contenant les images
  • _imagesFilesData : Les fichiers images (partiellement affichées dans la liste)
  • LoadFilesCommand : L’action de rechargement des fichiers depuis le répertoire

Voici le code source de MainWindowViewModel qui sera “bindé” sur la vue MainWindow

namespace WpfListViewMVVMSample.ViewModel
{
public class MainWindowViewModel : INotifyPropertyChanged
{
private ImageFileCollectionViewModel<ImageFileViewModel> _imagesFilesData;
private string _imagesPath;

public ICommand LoadFilesCommand { get; private set; }

public string ImagesPath
{
get { return _imagesPath; }
set
{
_imagesPath
= value;
RaisePropertyChanged(
"ImagesPath");
}
}

public ImageFileCollectionViewModel<ImageFileViewModel> ImagesFilesData
{
get
{
return _imagesFilesData;
}
}

public MainWindowViewModel()
{
_imagesFilesData
= new ImageFileCollectionViewModel<ImageFileViewModel>();
ImagesPath
= Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
LoadFilesCommand
= new SimpleDelegateCommand(
(x)
=> { InitImagesFilesDataWithDirectory(_imagesPath); });
}

private void InitImagesFilesDataWithDirectory(string directory)
{
var files
= from file in Directory.GetFiles(directory, "*.*")
where file.ToUpper().EndsWith(".JPG")
select
new ImageFileViewModel()
{ImageFile
= new ImageFile() {FileName = file} };
foreach (var img in files)
{
_imagesFilesData.AllImages.Add(img);
}
}

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
PropertyChanged(
this, new PropertyChangedEventArgs(propertyName));
}
}
}

LoadFilesCommand est initialisé avec un SimpleDelegateCommand décrit ci dessous. Il permet de lancer sous forme de delegate une action et de traiter ainsi le chargement de la liste depuis le texte saisi.

SimpleDelegateCommand peut être utilisé pour n’importe quelle action d’une commande nécessitant de personnaliser le “execute” avec un délégué.

public class SimpleDelegateCommand: ICommand
{
Action
<object> _executeDelegate;


public SimpleDelegateCommand(Action<object> executeDelegate)
{
_executeDelegate
= executeDelegate;
}

public void Execute(object parameter)
{
_executeDelegate(parameter);
}

public bool CanExecute(object parameter)
{
return true;
}

public event EventHandler CanExecuteChanged;
}

Binding des données sur le XAML

Le code XAML n’a plus qu’à récupérer le binding de la zone de texte, de la commande du bouton et de la liste des fichiers :

<Window x:Class="WpfListViewMVVMSample.MainWindow"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m
="clr-namespace:ImageFileListView.View;assembly=ImageFileListView"
Title
="MainWindow" Height="350" Width="525">
<Grid>
<Border x:Uid="Border_1" BorderBrush="#FF4355C1" BorderThickness="2,2,2,2"
CornerRadius
="4,4,4,4" Margin="8,60,2,8">
<m:ImageFileListView x:Name="listViewImages" ClipToBounds="False"
DataContext
="{Binding ImagesFilesData}" />
</Border>
<Button Content="Ok" Height="23" Margin="0,22,12,0" Name="button1"
VerticalAlignment
="Top" HorizontalAlignment="Right" Width="95"
Command
="{Binding LoadFilesCommand}"/>
<TextBox Height="23" Margin="12,22,113,0" Name="textBox1" VerticalAlignment="Top"
Text
="{Binding ImagesPath}"/>
</Grid>
</Window>

Résultat

Application exemple avec une liste d'images simple

Ajout de la gestion des répertoires

Nous allons modifier l’interface pour prendre en charge la gestion des répertoires par une case à cocher. Lorsque la case sera cochée, la liste contiendra les répertoires, lorsqu’elle sera décochée elle n’affichera que les images

Pour cela nous ajoutons un balise CheckBox qui sera “Bindé” sur une propriété ViewModel: IsDirectory

<CheckBox Content="Directories" Height="16" HorizontalAlignment="Left" 
Margin
="20,50,0,0" VerticalAlignment="Top"
IsChecked
="{Binding IsDirectory}" />

Dans le MainWindowViewModel, nous ajoutons la propriété IsDirectory

public bool IsDirectory
{
get { return _isDirectory; }
set
{
_isDirectory
= value;
RaisePropertyChanged(
"IsDirectory");
}
}

Dans la Commande du bouton OK, nous modifions l’appel :

public MainWindowViewModel()
{
_imagesFilesData
= new ImageFileCollectionViewModel<ImageFileViewModel>();
ImagesPath
= Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
LoadFilesCommand
= new SimpleDelegateCommand(
(x)
=> { InitImagesFilesDataWithDirectory(ImagesPath, IsDirectory); });
}

 

InitImagesFilesDataWithDirectory appelle soit

  • la liste des images ou soit
  • la liste des images avec les répertoires récursifs.

Le code ne présente pas d’intérêt ici et se trouve dans l’exemple.

Résultat avec les répertoires

Application M-V-VM exemple avec Liste en arbre de type TreeView

 

Téléchargements et exemples

Voici le lien pour télécharger la solution avec le code source du projet et des tests unitaires

download32x32 WPFImageFileListView.zip [449 ko]

et le lien pour juste observer le résultat avec cette petite application

download32x32 WpfListViewMVVMSample.exe

Last Updated on Friday, 15 April 2011 22:47
 

How to convert XHTML to PDF in C#

E-mail Print PDF
User Rating: / 68
PoorBest 
There are no translations available.

In this blog post I will illustrate how you could convert an XHTML page into PDF using the flying-saucer library. This is a Java library so we need to first step would be to convert it to a .NET assembly.

I will use the IKVM.NET Bytecode Compiler (ikvmc.exe) for this purpose. So go ahead and download both the flying-saucer library and the IKVM project. Then run the following command:

ikvmc.exe -target:library -out:CoreRenderer.dll iText-2.0.8.jar core-renderer.jar

This will generate the CoreRenderer.dll assembly which could be used. And finally we would create an application which would use it inside a console application:


One last step is to ensure that you have referenced the following assemblies:

todelka


Those assemblies are part of the IKVM project and have dependencies on many other assemblies that are included with it. So the final folder should contain all those:

todelka2

And that’s pretty much all. When you run the console application it should generate the output PDF file.

Last Updated on Wednesday, 02 March 2011 22:36
 

What features would you like to see in ASP.NET MVC 4?

E-mail Print PDF
User Rating: / 88
PoorBest 
There are no translations available.

If there was a single feature I would like to see in ASP.NET MVC 4 that would be to remove/deprecate ViewBag/ViewData. Those two constructs lead to very ugly code in the views and should be avoided. Here are few of the things I hate about them:

 

  • They are not strongly typed and you need to cast in your views in order to obtain the actual type
  • They are not refactor friendly because they rely on magic strings
  • They lead to brittle unit tests because of the magic strings
  • They lead to spaghetti code in the views

 

Here’s the diff patch I would love to see applied for the ViewDataDictionary.cs class in ASP.NET MVC 4:

 

diff --git ViewDataDictionary.cs ViewDataDictionary.cs
index 4c6299f..1965e3e 100644
--- ViewDataDictionary.cs
+++ ViewDataDictionary.cs
@@ -95,12 +95,10 @@ namespace System.Web.Mvc {
 
         public object this[string key] {
             get {
-                object value;
-                _innerDictionary.TryGetValue(key, out value);
-                return value;
+                throw new Expcetion("Don't use ViewData");
             }
             set {
-                _innerDictionary[key] = value;
+                throw new Expcetion("Don't use ViewData");
             }
         }
 
Last Updated on Sunday, 20 February 2011 12:40
 

Sample MVC project (updated)

E-mail Print PDF
User Rating: / 97
PoorBest 
There are no translations available.

I’ve updated the sample MVC project I wrote for using ASP.NET MVC 3 and the Razor view engine.

A minor change is that I no longer use the FluentValidationModelValidatorProvider but the standard one. So I decorate the view model with the necessary data annotations:

 

[Validator(typeof(UserViewModelValidator))]
public class UserViewModel
{
    public int Id { get; set; }

    [DisplayName("First name *")]
    public string FirstName { get; set; }

    [DisplayName("Last name *")]
    public string LastName { get; set; }

    public int? Age { get; set; }
}

 

And the validator becomes:

 

public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
    public UserViewModelValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty()
            .WithMessage("First name is required");

        RuleFor(x => x.LastName)
            .NotEmpty()
            .WithMessage("Last name is required");
    }
}

 

 

And here are the corresponding views using Razor:

 

Index.cshtml

@model IEnumerable<SampleMvc.Web.Models.UserViewModel>

@{
    ViewBag.Title = "Index";
}

<h2>Indexh2>
@(Html
    .Grid<UserViewModel>(Model)
    .Columns(column => {
        column.Custom(model => Html.Partial("_TableLinks", model));
        column.For(model => model.FirstName);
        column.For(model => model.LastName);
        column.For(model => model.Age);
    })
)

<p>
    @(Html.ActionLink<UsersController>(c => c.New(), "Create New"))
p>

 

_TableLinks.cshtml

@model SampleMvc.Web.Models.UserViewModel

@(Html.ActionLink<UsersController>(c => c.Edit(Model.Id), "Edit")) |
@(Html.ActionLink<UsersController>(c => c.Show(Model.Id), "Details")) |
@using (Html.BeginForm<UsersController>(c => c.Destroy(Model.Id))) 
{
    @Html.HttpMethodOverride(HttpVerbs.Delete)
    <input type="submit" value="Delete" />
}

 

Edit.cshtml

@model SampleMvc.Web.Models.UserViewModel
@{
    ViewBag.Title = "Edit";
}

<h2>Edith2>
@using (Html.BeginForm<UsersController>(c => c.Update(null)))
{
    @Html.ValidationSummary(true)
    @Html.HttpMethodOverride(HttpVerbs.Put)
    @Html.HiddenFor(model => model.Id)
    @Html.EditorForModel()
    <p>
        <input type="submit" value="Save" />
    p>
}
<div>
    @(Html.ActionLink<UsersController>(c => c.Index(), "Back to List"))
div>

 

New.cshtml

@model SampleMvc.Web.Models.UserViewModel

@{
    ViewBag.Title = "New";
}

<h2>Newh2>
@using (Html.BeginForm<UsersController>(c => c.Create(null))) 
{
    @Html.ValidationSummary(true)
    @Html.EditorForModel()
    <p>
        <input type="submit" value="Create" />
    p>
}
<div>
    @(Html.ActionLink<UsersController>(c => c.Index(), "Back to List"))
div>

 

Show.cshtml

@model SampleMvc.Web.Models.UserViewModel
           
@{
    ViewBag.Title = "Show";
}

<h2>Showh2>

@Html.DisplayForModel()
<p>
    @(Html.ActionLink<UsersController>(c => c.Edit(Model.Id), "Edit")) |
    @(Html.ActionLink<UsersController>(c => c.Index(), "Back to List"))
p>

 

UserViewModel.cshtml editor template

@model SampleMvc.Web.Models.UserViewModel

<fieldset>
    <legend>Fieldslegend>
            
    <div class="editor-label">
        @Html.LabelFor(model => model.FirstName)
    div>
    <div class="editor-field">
        @Html.EditorFor(model => model.FirstName)
        @Html.ValidationMessageFor(model => model.FirstName)
    div>
            
    <div class="editor-label">
        @Html.LabelFor(model => model.LastName)
    div>
    <div class="editor-field">
        @Html.EditorFor(model => model.LastName)
        @Html.ValidationMessageFor(model => model.LastName)
    div>
            
    <div class="editor-label">
        @Html.LabelFor(model => model.Age)
    div>
    <div class="editor-field">
        @Html.EditorFor(model => model.Age)
        @Html.ValidationMessageFor(model => model.Age)
    div>
fieldset>

UserViewModel.cshtml display template

@model SampleMvc.Web.Models.UserViewModel

<fieldset>
    <legend>Fieldslegend>
        
    <div class="display-label">Iddiv>
    <div class="display-field">@Html.DisplayFor(x => x.Id)div>
        
    <div class="display-label">FirstNamediv>
    <div class="display-field">@Html.DisplayFor(x => x.FirstName)div>
        
    <div class="display-label">LastNamediv>
    <div class="display-field">@Html.DisplayFor(x => x.LastName)div>
        
    <div class="display-label">Agediv>
    <div class="display-field">@Html.DisplayFor(x => x.Age)div>
fieldset>

 

The source code is available on github.

Last Updated on Sunday, 20 February 2011 11:33
 

Uploading multiple files with C#

E-mail Print PDF
User Rating: / 72
PoorBest 
There are no translations available.

Have you ever been in a situation where you needed to upload multiple files to a remote host and pass additional parameters in the request? Unfortunately there’s nothing in the BCL that allows us to achieve this out of the box.

We have the UploadFile method but it is restricted to a single file and doesn’t allow us to pass any additional parameters. So let’s go ahead and write such method. The important part is that this method must comply with RFC 1867 so that the remote web server can successfully parse the information.

First we define a model representing a single file to be uploaded:

    public class UploadFile
    {
        public UploadFile()
        {
            ContentType = "application/octet-stream";
        }
        public string Name { get; set; }
        public string Filename { get; set; }
        public string ContentType { get; set; }
        public Stream Stream { get; set; }
    }

And here’s a sample UploadFiles method implementation:

    public byte[] UploadFiles(string address, IEnumerable<UploadFile> files, NameValueCollection values)
    {
        var request = WebRequest.Create(address);
        request.Method = "POST";
        var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x", NumberFormatInfo.InvariantInfo);
        request.ContentType = "multipart/form-data; boundary=" + boundary;
        boundary = "--" + boundary;

        using (var requestStream = request.GetRequestStream())
        {
            // Write the values
            foreach (string name in values.Keys)
            {
                var buffer = Encoding.ASCII.GetBytes(boundary + Environment.NewLine);
                requestStream.Write(buffer, 0, buffer.Length);
                buffer = Encoding.ASCII.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"{1}{1}", name, Environment.NewLine));
                requestStream.Write(buffer, 0, buffer.Length);
                buffer = Encoding.UTF8.GetBytes(values[name] + Environment.NewLine);
                requestStream.Write(buffer, 0, buffer.Length);
            }

            // Write the files
            foreach (var file in files)
            {
                var buffer = Encoding.ASCII.GetBytes(boundary + Environment.NewLine);
                requestStream.Write(buffer, 0, buffer.Length);
                buffer = Encoding.UTF8.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"{2}", file.Name, file.Filename, Environment.NewLine));
                requestStream.Write(buffer, 0, buffer.Length);
                buffer = Encoding.ASCII.GetBytes(string.Format("Content-Type: {0}{1}{1}", file.ContentType, Environment.NewLine));
                requestStream.Write(buffer, 0, buffer.Length);
                file.Stream.CopyTo(requestStream);
                buffer = Encoding.ASCII.GetBytes(Environment.NewLine);
                requestStream.Write(buffer, 0, buffer.Length);
            }

            var boundaryBuffer = Encoding.ASCII.GetBytes(boundary + "--");
            requestStream.Write(boundaryBuffer, 0, boundaryBuffer.Length);
        }

        using (var response = request.GetResponse())
        using (var responseStream = response.GetResponseStream())
        using (var stream = new MemoryStream())
        {
            responseStream.CopyTo(stream);
            return stream.ToArray();
        }
    }

And here’s a sample usage:

 

    using (var stream1 = File.Open("test.txt", FileMode.Open))
    using (var stream2 = File.Open("test.xml", FileMode.Open))
    using (var stream3 = File.Open("test.pdf", FileMode.Open))
    {
        var files = new[] 
        {
            new UploadFile
            {
                Name = "file",
                Filename = "test.txt",
                ContentType = "text/plain",
                Stream = stream1
            },
            new UploadFile
            {
                Name = "file",
                Filename = "test.xml",
                ContentType = "text/xml",
                Stream = stream2
            },
            new UploadFile
            {
                Name = "file",
                Filename = "test.pdf",
                ContentType = "application/pdf",
                Stream = stream3
            }
        };

        var values = new NameValueCollection
        {
            { "key1", "value1" },
            { "key2", "value2" },
            { "key3", "value3" },
        };
        
        byte[] result = UploadFiles("http://localhost:1234/upload", files, values);
    }

 

 

 

 

 

In this example we are uploading 3 values and 3 files to the remote host.

Next time I will show how to improve this code by adding an asynchronous version using the TPL library in .NET 4.0.

Last Updated on Saturday, 22 January 2011 01:25
 

How we do ASP.NET MVC

E-mail Print PDF
User Rating: / 179
PoorBest 
There are no translations available.

Sample MVC Solution

In this post I will show a sample ASP.NET MVC 2.0 project structure illustrating different concepts such as data access, user input validation and mapping between the domain and the view model. The project is still under construction but the source code is available at github.

I will illustrate the usage of the following frameworks:

  • MvcContrib bringing useful extension methods and strongly typed helpers to ASP.NET MVC
  • AutoMapper enabling easy mapping between the domain and the view models
  • FluentValidation - a small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects
  • NHibernate – a popular ORM in the .NET world
  • FluentNHibernate - a statically compiled alternative to NHibernate's standard hbm xml mapping
  • Spring.NET – object container and dependency Injection framework for .NET
  • Rhino.Mocks - A dynamic mock object framework for the .Net platform. It's purpose is to ease testing by allowing the developer to create mock implementations of custom objects and verify the interactions using unit testing

 

Armed with this arsenal of frameworks let’s start exploring the solution structure. I’ve opted for 2 projects solution but in many real world applications more levels of abstraction could be brought to the business layer. Personally I favor to have less big assemblies rather than many small assemblies into the solution. Fewer the assemblies, faster the load time and faster the IDE. In this case particular attention should be brought to bring proper separation of concerns inside the same assembly

 

ASP.Net MVC project structure

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The domain consists of a single User class and a repository interface defining the different operations on this model:

ASP.Net MVC Domain with Repository

 

Mapping

The next step is to define the mapping between our domain and a relational model expressed in a fluent manner:

public class UserMap : ClassMap<User>
{
public UserMap()
{
Table(
"users");
Id(x
=> x.Id, "usr_id");
Map(x
=> x.FirstName, "usr_firstname");
Map(x
=> x.LastName, "usr_lastname");
Map(x
=> x.Age, "usr_age");
}
}

 

And here’s the implementation of the repository:

public class SqlUsersRepository : HibernateDaoSupport, IUsersRepository
{
public IEnumerable<User> GetUsers()
{
return HibernateTemplate.LoadAll<User>();
}

public User Get(int id)
{
return HibernateTemplate.Get<User>(id);
}

public void Delete(int id)
{
HibernateTemplate.Delete(
new User { Id = id });
}

public int Save(User user)
{
return (int)HibernateTemplate.Save(user);
}

public void Update(User user)
{
HibernateTemplate.Update(user);
}
}

 

HibernateDaoSupport is a base class defined by the Spring Framework managing SQL transactions and NHibernate session.

Once we have implemented the data access layer we could move on to the web part. The application consists of a single RESTful UsersController allowing the standard CRUD operations with our users model. As all our views are strongly typed we shall define a view model for each view and a mapping between the domain model and this view model. In our simple case the view model will simply have the same structure as the domain model but in real world scenarios it will be a projection of the domain model for a particular view.

[Validator(typeof(UserViewModelValidator))]
public class UserViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int? Age { get; set; }
}

And the respective validator:

public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
public UserViewModelValidator()
{
RuleFor(x
=> x.FirstName)
.NotEmpty()
.WithMessage(
"First name is required")
.DisplayName(
"First name *");

RuleFor(x
=> x.LastName)
.NotEmpty()
.WithMessage(
"Last name is required")
.DisplayName(
"Last name *");
}
}

And mapper between the domain and view model:

 

public class UserMapper : IMapper
{
static UserMapper()
{
Mapper.CreateMap
<User, UserViewModel>();
Mapper.CreateMap
<UserViewModel, User>();
}

public object Map(object source, Type sourceType, Type destinationType)
{
return Mapper.Map(source, sourceType, destinationType);
}
}

 

This bidirectional mapper will be used by our RESTful controller:

public class UsersController : BaseController<IUsersRepository>
{
public UsersController(IUsersRepository repository, IMapper userMapper)
:
base(repository, userMapper)
{ }

[AutoMap(
typeof(IEnumerable<User>), typeof(IEnumerable<UserViewModel>))]
public ActionResult Index()
{
// return all users
var users = Repository.GetUsers();
return View(users);

}

public ActionResult New()
{
// return an HTML form for describing a new user
return View();
}

[HttpPost]
[AutoMap(
typeof(User), typeof(UserViewModel))]
public ActionResult Create(UserViewModel userView)
{
// create a new user
if (!ModelState.IsValid)
{
return View("New", userView);
}
var user
= (User)ModelMapper.Map(userView, typeof(UserViewModel), typeof(User));
Repository.Save(user);
return RedirectToAction("Index", "Users");
}

[AutoMap(
typeof(User), typeof(UserViewModel))]
public ActionResult Show(int id)
{
// find and return a specific user
var user = Repository.Get(id);
return View(user);
}

[AutoMap(
typeof(User), typeof(UserViewModel))]
public ActionResult Edit(int id)
{
// return an HTML form for editing a specific user
var user = Repository.Get(id);
return View(user);
}

[HttpPut]
public ActionResult Update(UserViewModel userView)
{
// find and update a specific user
if (!ModelState.IsValid)
{
return View("Edit", userView);
}
var user
= (User)ModelMapper.Map(userView, typeof(UserViewModel), typeof(User));
Repository.Update(user);
return RedirectToAction("Index", "Users");
}

[HttpDelete]
public ActionResult Destroy(int id)
{
// delete a specific user
Repository.Delete(id);
return RedirectToAction("Index", "Users");
}
}

 

Notice the AutoMapAttribute. This is a custom attribute allowing us to automatically convert the domain model retrieved by the repository to a view model and present it to the view:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class AutoMapAttribute : ActionFilterAttribute
{
public Type SourceType { get; private set; }
public Type DestType { get; private set; }

public AutoMapAttribute(Type sourceType, Type destType)
{
SourceType
= sourceType;
DestType
= destType;
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var controller
= filterContext.Controller as IModelMapperController;
if (controller == null)
{
return;
}
var model
= filterContext.Controller.ViewData.Model;
if (model != null && SourceType.IsAssignableFrom(model.GetType()))
{
var viewModel
= controller.ModelMapper.Map(model, SourceType, DestType);
filterContext.Controller.ViewData.Model
= viewModel;
}
}
}

The OnActionExecuted method will be called after each action method has finished executing and it will use the model passed to the view and convert it to the appropriate view model. It simply substitutes the ViewData.Model property with the appropriate view model to finally pass it to the view for rendering.

 

The controller follows the standard RESTful conventions for naming the action and the HTTP verbs:

 

URL

HTTP Verb

Action

Description

/users/index GET Index() return all users
/users/show/id GET Show(int id) return a specific user
/users/new GET New() return an HTML form for creating a new user
/users/create POST Create(UserViewModel userView) create a new user
/users/edit/id GET Edit(int id) return an HTML form for editing a specific user
/users/update PUT Update(UserViewModel userView) update a specific user
/users/destroy/id DELETE Destroy(int id) delete a specific user

 

Because most browsers support submitting HTML forms only using the GET and POST verbs, there’s the Html.HttpMethodOverride helper which generates a hidden field inside the form and is used by the routing engine to dispatch to the proper controller action.

Unit Tests

Unit testing our controller actions is essential. I’ve been using the excellent MVCContrib.TestHelper in conjunction with the Rhino.Mocks framework to test controllers in isolation by mocking the HTTP context. Here’s how the test logic looks like:

[TestClass]
public class UsersControllerTests : TestControllerBuilder
{
private UsersController _sut;
private IUsersRepository _repositoryStub;
private IMapper _userMapperStub;

public UsersControllerTests()
{
}

private TestContext testContextInstance;

///  
///Gets or sets the test context which provides
///information about and functionality for the current test run.
/// 
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance
= value;
}
}

// Use TestInitialize to run code before running each test
[TestInitialize()]
public void MyTestInitialize()
{
_repositoryStub
= MockRepository.GenerateStub<IUsersRepository>();
_userMapperStub
= MockRepository.GenerateStub<IMapper>();
_sut
= new UsersController(_repositoryStub, _userMapperStub);
InitializeController(_sut);
}

[TestMethod]
public void UsersController_Index()
{
// arrange
var users = new User[0];
_repositoryStub.Stub(x
=> x.GetUsers()).Return(users);

// act
var actual = _sut.Index();

// assert
actual
.AssertViewRendered()
.WithViewData
<User[]>()
.ShouldBe(users);
}

[TestMethod]
public void UsersController_New()
{
// act
var actual = _sut.New();

// assert
actual
.AssertViewRendered();
}

[TestMethod]
public void UsersController_Create_Invalid_Model_State()
{
// arrange
_sut.ModelState.AddModelError("FirstName", "First name is required");
var userView
= new UserViewModel();

// act
var actual = _sut.Create(userView);

// assert
actual
.AssertViewRendered()
.ForView(
"New")
.WithViewData
<UserViewModel>()
.ShouldBe(userView);
}

[TestMethod]
public void UsersController_Create_Success()
{
// arrange
var userView = new UserViewModel();
var user
= new User();
_userMapperStub
.Stub(x
=> x.Map(userView, typeof(UserViewModel), typeof(User)))
.Return(user);

// act
var actual = _sut.Create(userView);

// assert
actual
.AssertActionRedirect()
.ToAction
<UsersController>(c => c.Index());
_repositoryStub.AssertWasCalled(x
=> x.Save(user));
}

[TestMethod]
public void UsersController_Show()
{
// arrange
var id = 1;
var user
= new User();
_repositoryStub.Stub(x
=> x.Get(id)).Return(user);

// act
var actual = _sut.Show(id);

// assert
actual
.AssertViewRendered()
.WithViewData
<User>()
.ShouldBe(user);
}

[TestMethod]
public void UsersController_Edit()
{
// arrange
var id = 1;
var user
= new User();
_repositoryStub.Stub(x
=> x.Get(id)).Return(user);

// act
var actual = _sut.Edit(id);

// assert
actual
.AssertViewRendered()
.WithViewData
<User>()
.ShouldBe(user);
}

[TestMethod]
public void UsersController_Update_Invalid_Model_State()
{
// arrange
_sut.ModelState.AddModelError("FirstName", "First name is required");
var userView
= new UserViewModel();

// act
var actual = _sut.Update(userView);

// assert
actual
.AssertViewRendered()
.ForView(
"Edit")
.WithViewData
<UserViewModel>()
.ShouldBe(userView);
}

[TestMethod]
public void UsersController_Update_Success()
{
// arrange
var userView = new UserViewModel();
var user
= new User();
_userMapperStub
.Stub(x
=> x.Map(userView, typeof(UserViewModel), typeof(User)))
.Return(user);

// act
var actual = _sut.Update(userView);

// assert
actual
.AssertActionRedirect()
.ToAction
<UsersController>(c => c.Index());
_repositoryStub.AssertWasCalled(x
=> x.Update(user));
}

[TestMethod]
public void UsersController_Destroy()
{
// arrange
var id = 1;

// act
var actual = _sut.Destroy(id);

// assert
actual
.AssertActionRedirect()
.ToAction
<UsersController>(c => c.Index());
_repositoryStub.AssertWasCalled(x
=> x.Delete(id));
}
}

Views

And the last but not least part of the picture are the views:

Index.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="SampleMvc.Web.Models" %>
<%@ Import Namespace="SampleMvc.Web.Controllers" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Index
</h2>
 
<%: Html.Grid>(Model)
.Columns(column => {
column.For("TableLinks").Named("");
column.For(model => model.FirstName);
column.For(model => model.LastName);
column.For(model => model.Age);
})
%>
<p>
<%: Html.ActionLink>(c => c.New(), "Create New") %>
</p> 
</asp:Content> 

TableLinks.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="SampleMvc.Web.Controllers" %>
<td>
<%: Html.ActionLink>(c => c.Edit(Model.Id), "Edit") %> |
<%: Html.ActionLink>(c => c.Show(Model.Id), "Details") %> |
<% using (Html.BeginForm>(c => c.Destroy(Model.Id))) { %>
<%: Html.HttpMethodOverride(HttpVerbs.Delete) %>
<input type="submit" value="Delete" />
<% } %>
</td>
 

Edit.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="SampleMvc.Web.Controllers" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit
</asp:Content> 

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Edit
</h2>  
<% using (Html.BeginForm>(c => c.Update(null))) {%>
<%: Html.ValidationSummary(true) %>
<%: Html.HttpMethodOverride(HttpVerbs.Put) %>
<%: Html.HiddenFor(model => model.Id) %>
<%: Html.EditorForModel() %>
<p>
<input type="submit" value="Save" />
</p>
  <% } %>
<div>
<%: Html.ActionLink>(c => c.Index(), "Back to List") %>
</div> 
</asp:Content> 

New.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="SampleMvc.Web.Controllers" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
New
</asp:Content> 

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>New
</h2>
<% using (Html.BeginForm>(c => c.Create(null))) {%>
<%: Html.ValidationSummary(true) %>

<%: Html.EditorForModel() %>
<p>
<input type="submit" value="Create" />
</p>
<% } %>
<div>
<%: Html.ActionLink>(c => c.Index(), "Back to List") %>
</div>
</asp:Content> 

UserViewModel.ascx editor template

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>

<fieldset>
<legend>Fields</legend >
<div class="editor-label">
<%: Html.LabelFor(model => model.FirstName) %>
 </div>
 
<div class="editor-field">
<%: Html.TextBoxFor(model => model.FirstName) %>
<%: Html.ValidationMessageFor(model => model.FirstName) %>
 </div>
<div class="editor-label">
<%: Html.LabelFor(model => model.LastName) %>  

 </div>
 
<div class="editor-field">
<%: Html.TextBoxFor(model => model.LastName) %>
<%: Html.ValidationMessageFor(model => model.LastName) %>
 
 </div>
<div class="editor-label">
<%: Html.LabelFor(model => model.Age) %>
 </div>
 <div class="editor-field">
<%: Html.TextBoxFor(model => model.Age) %>
<%: Html.ValidationMessageFor(model => model.Age) %>
 
</div>
 
</fieldset>

A very important aspect of the views is that they are all strongly typed to a view model and use only strongly typed helpers, even for generating the links. One day when Visual Studio becomes power enough you will be able to seamlessly refactor/rename a property without worrying about all those magic strings.

 

Further enchantments will include adding client side validation using the jQuery validate plugin in order to improve the user experience and preserve bandwidth.

Last Updated on Friday, 14 May 2010 12:58
 

WPF, ListView d’images, MVVM et Drag and Drop… – Partie 3

E-mail Print PDF
User Rating: / 75
PoorBest 
There are no translations available.

Introduction

Vu précédemment

Nous avons vu dans la première partie comment implémenter une liste de fichiers en WPF avec le pattern MVVM.

Dans la deuxième partie, nous avons vus comment optimiser l’interface utilisateur avec le VirtualizingStackPanel, le multi Thread, les animations d’attente WPF. Nous avons eu également un aperçu de la manipulation des images pour utiliser lorsqu’elles existent les informations EXIF ou encore une transformation classique de ces images.

Je vous propose de voir dans cette troisième partie :

  • La mise en cache des images (améliorons l’interface utilisateur encore un petit peu)
  • Le Drag and Drop avec une sélection multiple d’éléments

Optimisation avec mise en cache

Implémentation d’un système de cache

Le but est de garder les x dernières images déjà chargée afin d’éviter un rechargement permanent de ces images.

Pour cela nous mettons en place une liste partagée en appel static, ainsi que son verrou (nous sommes dans un environnement multi-thread) afin de garantir l’écriture correcte des données.

const int maxThumbnailInCache = 300;
private static readonly List<ImageFileViewModel> _cacheThumbnail
= new List<ImageFileViewModel>();
private static readonly object _lockCacheThumbnail = new object();

Puis nous implémentons la méthode static AddThumbnailInCache

 

 private static void AddThumbnailInCache(ImageFileViewModel p)
{
if (p == null) return;
if (_cacheThumbnail.Contains(p))
{
lock (_lockCacheThumbnail)
{
_cacheThumbnail.Remove(p);
}
}

lock (_lockCacheThumbnail)
{
_cacheThumbnail.Add(p);
if (_cacheThumbnail.Count > maxThumbnailInCache)
lock (_lockCacheThumbnail)
{
_cacheThumbnail.Remove(_cacheThumbnail[
0]);
}
}
}

 

La méthode teste si l’élément est présent. Si il est présent, l’élément est replacé à la fin de la liste.

Si le nombre d’éléments dépasse le maximum d’élément fixés dans le cache (maxThumbnailInCache), l’élément en haut (le plus ancien) est supprimé.

Cette méthode est appelée lorsque l’image est chargée en mémoire.

 

 public ImageSource Thumbnail
{
get
{
if (_thumbnail == null)
{
LoadImage();
return null;
}

AddThumbnailInCache(
this);
return _thumbnail;
}
}

 

Nous modifions maintenant l’instruction de libération des éléments pour ne supprimer que ceux absent de la liste

 internal bool CleanUp()
{
if (!_cacheThumbnail.Contains(this))
{
lock(_lockCacheThumbnail)
{
lock (_lockListThumbnail)
{
_listThumbnail.Remove(
this);
_loading
= false;
_thumbnail
= null;
}
}
return false;
}
else
return true;
}

Puis dans la partie View, nous demandons à ne pas décharger l’élément si CleanUp n’a pas réussi (cas où l’on garde l’élément en cache).

 

namespace wpfListViewSample03.View
{
public partial class ImageFileListView : UserControl
{
public ImageFileListView()
{
InitializeComponent();
}

private void ListViewImage_CleanUpVirtualizedItem
(
object sender, CleanUpVirtualizedItemEventArgs e)
{
if (e.Value is ImageFileViewModel)
{
e.Cancel
= (e.Value as ImageFileViewModel).CleanUp();
}
}
}
}

 

Exécution :

tmp31B

Et voila, 316 éléments (300 en cache + 16 affichés) sont gardés en cache, la vignette (thumbnail) ne sera pas ainsi constamment régénérée.

Les tests unitaires

Les tests unitaires associés sont assez simples. Il faudra penser à ajouter le vidage de la liste _CacheThumbnail.

[TestInitialize()]
public void MyTestInitialize()
{
// Clear Thread processus
ImageFileViewModel_Accessor._loaderThreadThumbnail.Abort();
// call the static constructor
Type staticType = typeof(ImageFileViewModel);
ConstructorInfo ci
= staticType.TypeInitializer;
object[] parameters = new object[0];
ci.Invoke(
null, parameters);
// and clear static attribute _ListThumbnails and _cacheThumbnails
ImageFileViewModel_Accessor._listThumbnail.Clear();
ImageFileViewModel_Accessor._cacheThumbnail.Clear();
}

Le Drag And Drop dans une ListBox WPF avec M-V-VM

Drag and Drop, Quoi de plus ?

Le drag and drop de ListBox (ou ListView) a déjà été traité dans de nombreux articles de qualité dont je me suis inspiré pour réaliser cette prochaine version.

Voici la liste de ces articles

Ce qui manquait, une version générale (chaque petit bout étant dans dans la plupart de ces articles) avec

  • La gestion du déplacement d’un Item dans une ListBox ou ListView. La plupart font une copie des éléments mais ne gèrent pas les déplacements
  • La multi-sélection, la copie ou le déplacements de plusieurs éléments sélectionnés.
  • Le scroll automatique lorsque l’on déplace les éléments en dehors de la fenêtre
  • Une séparation entre la gestion du drag and drop et la couche visuelle d’affichage des Adorners (Aspect graphique indiquant la position des éléments glissés)

J’ai donc réalisé un premier “DragAndDropManager” sans les fioritures graphiques puis un héritier qui va uniquement implémenter les aspect d’affichage Adorners.

Organisation du projet Drag And Drop Manager.

Toute la gestion du drag and drop se retrouve dans un projet unique DragDropManager qui pourra être repris dans un projet indépendant pour vos ListView ou ListBox.

L’organisation du projet est la suivante

tmpAA

La partie Model n’est pas spécifique WPF.

Elle contient 2 classes

  • DragDropManagerList Le gestionnaire de DragAndDrop permettant de gérer des ensembles, et les mécanismes de début, fin d’opérations de DragAndDrop.
  • MouseUtilities inclue une fonction permettant de localiser le curseur de la souris au moment du Drag. Il faut savoir que la grosse difficulté lors du drag and drop est de pouvoir gérer la position du curseur. Microsoft en effet n’a pas prévu jusqu’à la version 3.5 de pouvoir suivre le mouvement de la souris lors de cette opération.

La partie ViewModel contient 2 classes principales

  • DragDropManagerListBoxViewModel gère les fonctions de drag et drop de la souris avec les composants WPF ListBox. Elle dérive de la classe DragDropManagerList en utilisant ces fonctionnalités de base de DragDropManagerList. Cette classe peut être utilisée si vous n’avez pas besoin des Adorners
  • DragDropManagerListBoxAdornerViewModel gère l’affichage des 2 Adorners

2 classes définissent les Adorners

  • DropAdorner affiche la partie visuelle (vignette + libellé de la sélection) en transparence sous le curseur de la souris
  • InsertionAdorner affiche une ligne noire indiquant l’emplacement destination des éléments à insérer dans la liste.

Les 2 classes Adorners ont été reprises des projets cités plus haut. Il est possible d’écrire ses propres Adorners pour personnaliser le type d’affichage

image

L’ensemble des classes permettant la gestion du DragAndDrop sont résumées ici

 

image

 

La gestion des éléments multiples dans le Drag en Drop

Le drag and drop d’éléments multiples va passer par une structure Dictionary _draggedItems.

La clé du dictionnaire va contenir la position de l’élément et sa valeur contiendra l’objet de type T Générique.

 

protected Dictionary<int, T> _draggedItems = new Dictionary<int, T>();

 

La classe DragDropManagerList implémente ensuite 3 méthodes statiques permettant de gérer le Drop des éléments sélectionnés (_draggedItems).

  • DoDrop sera appelé au moment du Drop des éléments dans la liste. Il retourne les nouveaux éléments copiés sous forme d’un dictionnaire (SI les éléments sont déplacés retourne null). En fonction de la touche de clavier Ctrl utilisé, les éléments seront soit copiés soit déplacés.
  • CopyDraggedItems gère la copie des éléments
  • MoveDraggedItems gère le déplacement des éléments
protected static Dictionary<int, T> DoDrop(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems
,
int index, DragDropEffects effects, DragDropKeyStates keyStates)
{
T dropTarget
= itemsSource[index];
if (draggedItems.ContainsValue(dropTarget)) return null;

if ((DragDropEffects.Move & effects) == DragDropEffects.Move
&& (DragDropKeyStates.ControlKey & keyStates)
!= DragDropKeyStates.ControlKey)
effects
= DragDropEffects.Move;
if ((DragDropEffects.Copy & effects) == DragDropEffects.Copy)
effects
= DragDropEffects.Copy;

switch (effects)
{
case DragDropEffects.Move:
{
MoveDraggedItems(itemsSource, draggedItems, index);
draggedItems.Clear();
return null;
}

case DragDropEffects.Copy:
{
Dictionary
<int, T> newItems = CopyDraggedItems(itemsSource, draggedItems, index);
draggedItems.Clear();
return newItems;
}
}
return null;
}

protected static Dictionary<int, T> CopyDraggedItems(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems, int index)
{
Dictionary
<int, T> newItems = new Dictionary<int, T>();
int i = 0;
foreach (int key in draggedItems.Keys.OrderBy(ii => ii))
{
T draggedItemClone
= draggedItems[key].Clone() as T;
if (draggedItemClone != null)
{
itemsSource.Insert(index
+ i, draggedItemClone);
newItems.Add(index
+ i, draggedItemClone);
i
++;
}
}
return newItems;
}

protected static void MoveDraggedItems(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems, int index)
{
int dest = index;
int origin = 0;
Dictionary
<int, T> completedDraggedItems = draggedItems;
T ModelDest
= itemsSource[dest];
foreach (int key in completedDraggedItems.Keys)
{
origin
= itemsSource.IndexOf(completedDraggedItems[key]);
if (itemsSource[dest] != completedDraggedItems[key])
{
if (draggedItems.ContainsKey(key))
{
ModelDest
= draggedItems[key];
}
if (key > index)
dest
++;
itemsSource.Move(origin, dest);
}
}
}

La classe implémente le mécanisme de gestion du déplacement visuel des éléments durant le Drag.

Les méthodes seront surchargées dans les classes héritées pour gérer les particularités visuelles (ListBox, Adorners,…).

protected void DoDragOperation()
{
InitializeDragOperation();
try
{
PerformDragOperation();
}
finally
{
FinishDragOperation();
}
}

La gestion du Drag and Drop en M-V-VM

L’initialisation du DragAndDropManager va se faire de la façon suivante dans la couche View.

public ImageFileListView()
{
InitializeComponent();
new DragDropManagerListBoxViewModel
<ImageFileViewModel>(ListViewImage);
}

 

Le constructeur initialise le composant visuel ListBox ListViewImage avec le type objet ImageFileViewModel.

Dans la classe DragDropManagerListBoxViewModel, les évènements suivants sont attrapés et redirigés pour la gestion du Drag and Drop.

 

public virtual ListBox ListBox
{
get {return _listBox;}
set
{
if (_listBox != null)
{
_listBox.PreviewMouseMove
-= listBoxPreviewMouseMove;
_listBox.PreviewMouseLeftButtonDown
-= listBoxPreviewMouseLeftButtonDown;
_listBox.PreviewMouseLeftButtonUp
-= listBoxPreviewMouseLeftButtonUp;
_listBox.DragOver
-= listBoxDragOver;
_listBox.Drop
-= listBoxDrop;
}
_listBox
= value;
_listBox.AllowDrop
= true;
if (_listBox != null)
{
if (!_listBox.AllowDrop)
_listBox.AllowDrop
= true;
_listBox.PreviewMouseMove
+= listBoxPreviewMouseMove;
_listBox.PreviewMouseLeftButtonDown
+= listBoxPreviewMouseLeftButtonDown;
_listBox.PreviewMouseLeftButtonUp
+= listBoxPreviewMouseLeftButtonUp;
_listBox.DragOver
+= listBoxDragOver;
_listBox.Drop
+= listBoxDrop;

}
}
}
  • PreviewMouseLeftButtonDown : Bouton de la souris enfoncé : utilisé pour initialisé l’opération de glisser (drag) et déterminer les éléments à placer dans _draggedItems.
  • PreviewMouseMove : déplacement de la souris :  utilisé pour l’affichage des Adorners, l’affichage du curseur (selon appuie du bouton CTRL et la sélection de l’item de destination
  • DragOver : Survol des éléments lorsque les items sont sélectionnés. Permet de rafraichir les Adorners et de gérer le déplacement de la barre de scrolling lorsque le curseur est proche des limites Haut et Bas.
  • Drop : Action définitive du Drag and Drop, Copie ou déplacement des éléments
  • PreviewMouseLeftButtonUp : Bouton de la souris relâché, vide les éléments sélectionnés (_draggedItems.Clear) .

Le déplacement automatique des éléments durant le Drag and Drop

protected virtual void listBoxDragOver(object sender, DragEventArgs e)
{
AutoScrollCursorBounds();
}

private void AutoScrollCursorBounds()
{
Point ptMouse
= DragDropManager.MouseUtilities.GetMousePosition(_listBox);
Rect bounds
= VisualTreeHelper.GetDescendantBounds(_listBox);
if (ptMouse.Y > bounds.Height - 10 || ptMouse.Y < 10)
_listBox.ScrollIntoView(GetCurrentListBoxItem());
}

Durant le déplacement des éléments sélectionnés, la méthode AutoScrollCursorBounds() est appelée. Cette méthode regarde la position de la Souris, la compare aux bords de la liste et réalise le scroll sur l’élément sélectionné si elle est proche d’un bord.

La gestion des Adorners (InsertionAdorner et DropAdorner)

Elle s’effectue avec la classe DragDropManagerListBoxAdornerViewModel.

InsertionAdorner et DropAdorner sont 2 classes trouvées sur les différents forums.

Josh Smith a écrit le DropAdorner et l’InsertionAdorner provient du site de bea Stollnitz.

 

image

La création de ces 2 descendants de Adorners intervient à 2 moment différents.

Le DropAdorner est créé dès le début de l’opération de Drag (PreviewMouseMove appelle DoDragOperation qui lance InitializeDragOperation()) pour afficher la vignette des éléments en déplacement.

 

protected override void InitializeDragOperation() 
{
base.InitializeDragOperation();
if (ListBox.SelectedItem != null)
{
T draggeditem
= ListBox.SelectedItem as T;
if (draggeditem != null)
{
ListBoxItem lvi
= GetVisualItem(draggeditem) as ListBoxItem;
if (lvi != null)
CreateAdornerLayer(lvi);
}
}
}

 

Le InsertionAdorner est créé uniquement au survol d’un élément sélectionnable et détruit dès que le curseur change de ligne.

 

private void ListBoxDragEnter(object sender, DragEventArgs e)
{
if (dragAdorner != null && dragAdorner.Visibility != Visibility.Visible)
{
UpdateDragAdornerLocation();
dragAdorner.Visibility
= Visibility.Visible;
}

object draggedItem = e.Data.GetData(format.Name);
if (draggedItem != null)
{
CreateInsertionAdorner();
}
}

private void ListBoxPreviewDragLeave(object sender, DragEventArgs e)
{
object draggedItem = e.Data.GetData(format.Name);
if (draggedItem != null)
{
RemoveInsertionAdorner();
}
}

 

Lors de l’arrêt de la suppression du déplacement par relâchement de la souris, les Adorners sont supprimés.

 

protected override void FinishDragOperation()
{
RemoveDragAdorner();
RemoveInsertionAdorner();
base.FinishDragOperation();
}

 

Au cours du déplacement du pointeur de la souris, la position du DropAdorner est mise à jour.

Le nouveau positionnement est calculé et passé à l’objet Adorner (dragAdorners.SetOffsets(left, top);

 

private void UpdateDragAdornerLocation()
{
if (this.dragAdorner != null)
{
Point ptCursor
= DragDropManager.MouseUtilities.GetMousePosition(ListBox);
double left = ptCursor.X - ptMouseDown.X;
double top = ptCursor.Y;
dragAdorner.SetOffsets(left, top);
}
}

 

 

Le DragAndDrop avec Adorners s’utilise simplement, vous devez utiliser la classe DragDropManagerListBoxAdornerViewModel dans le constructeur de la façon suivante :

public ImageFileListView()
{
InitializeComponent();
new DragDropManagerListBoxAdornerViewModel
<ImageFileViewModel>(ListViewImage);
}

Le problème de sélection avec la multi-sélection et le Drag and Drop

Vous l’avez peut être déjà remarqué, la multi-sélection pose un problème avec le Drag and Drop dans une LisView.

Le bouton de la souris lorsqu’il est pressé au moment du Drag sur un élément déjà sélectionné provoque la dé-sélection de tous les autres éléments.

image image

Comme il s’agit d’un problème de composant visuel WPF, nous allons traiter ce disfonctionnement dans la partie View en modifiant le comportement du ListViewItem.

Pour cela il faut surcharger la classe ListViewItem et modifier le type d’éléments renvoyé par le ListView


public class ListViewMultiSelect : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ListViewItemMultiSelect();
}
}

Ensuite, le nouveau ListViewItem (ici ListViewItemMultiSelect) modifie le comportement du click.

  • Si l’élément n’est pas déjà sélectionné, la sélection est faite normalement à la pression du bouton de la souris
  • Si l’élément est sélectionné, rien n’est fait au moment de  la pression du bouton (afin de garder les autres éléments sélectionnés) et la gestion de la sélection est reportée au moment du relâchement du bouton de la souris.

 

public class ListViewItemMultiSelect : ListViewItem
{
private bool _mouseWasDownAndSelected = false;
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (!IsSelected)
{
// it wasn't selected, so just do the normal thing
base.OnPreviewMouseLeftButtonDown(e);
return;
}
// it was selected, so we're going to totally ignore the mouse down...
e.Handled = true;
// but we will mark it ...
_mouseWasDownAndSelected = true;
}

protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
// if we were watching this one, we'll unselect it if it were already selected
if (IsSelected && _mouseWasDownAndSelected)
{
IsSelected
= _mouseWasDownAndSelected = false;
}
base.OnMouseLeftButtonUp(e);
}
}

 

Il ne reste plus qu’à modifier le UserControl ImageFileListView.xaml de la partie View du projet pour utiliser ce nouveau composant Multi Select.

 

<local:ListViewMultiSelect SelectionMode="Extended" ... >

 

Tests unitaires

Les tests unitaires du Drag and Drop

Le drag and drop peut être testé avec les tests unitaires grâce au découpage MVVM.

Une classe SimpleTestClass a été spécialement créée afin de réaliser les tests

 

public class SimpleTestClass : Object, ICloneable
{
public object Clone()
{
return new SimpleTestClass();
}
}

 

Le test unitaire peut ainsi être implémenté

[TestMethod()]
[DeploymentItem(
"DragDropManager.dll")]
public void MoveDraggedItemsTest()
{
ObservableCollection
<SimpleTestClass> collection =
new ObservableCollection<SimpleTestClass>();
SimpleTestClass a1
= new SimpleTestClass();
SimpleTestClass a2
= new SimpleTestClass();
SimpleTestClass a3
= new SimpleTestClass();
SimpleTestClass a4
= new SimpleTestClass();
collection.Add(a1);
collection.Add(a2);
collection.Add(a3);
collection.Add(a4);
Dictionary
<int, SimpleTestClass> newItems =
new Dictionary<int, SimpleTestClass>();
newItems.Add(
3, collection[3]);
newItems.Add(
1, collection[1]);
// Test Count and Order moved Items
DragDropManagerList_Accessor<SimpleTestClass>
.MoveDraggedItems(collection, newItems,
2);
Assert.AreEqual(
4, collection.Count);
Assert.AreEqual(a1, collection[
0]);
Assert.AreEqual(a3, collection[
1]);
Assert.AreEqual(a4, collection[
2]);
Assert.AreEqual(a2, collection[
3]);
}

Les autres tests unitaires se retrouvent dans le projet zip.

Les tests unitaires de la partie View WPF

Ce type de test n’est pas courant car normalement le modèle M-V-VM doit nous permettre d’éviter de tester un comportement WPF.

Seulement ici dans le cas du drag and drop, nous souhaitons tester un comportement essentiel de la fonction GetVisualItem.

Cette fonction est très utilisée pour faire le lien entre le curseur de la souris et la donnée pointée par ce curseur.

GetVisualItem permet de récupérer l’élément visuel WPF correspondant à la donnée object “Bindé” dans le composant visuel. Exemple GetVisualItem(data1) va retourner le sous élément de type ListBoxItem contenant l’objet data1.

Pour tester de façon unitaire cette fonction, il faut donc créer un ObservableCollection qui contiendra les données et une ListBox UI WPF qui contiendra les éléments affichés. Comme nous sommes dans un test unitaire, il n’y a malheureusement pas création de fenêtre WPF ni donc de génération interne des composants ListBoxItem liés à l’ObservableCollection. Ainsi le code suivant ne fonctionnera pas car le composant ListBox ne contiendra pas d’UI éléments générés.

 public void GetVisualItemTest1Helper<T>()
where T : class, ICloneable
{
ListBox listbox
= new ListBox();
T testClass
= new Object() as T;
ObservableCollection
<T> collection = new ObservableCollection<T>();
collection.Add(testClass);
listbox.ItemsSource
= collection;

DragDropManagerListBoxViewModel
<T> dragdropManagerListBoxViewModel = new DragDropManagerListBoxViewModel<T>(listbox);
PrivateObject param0
= new PrivateObject(dragdropManagerListBoxViewModel);
DragDropManagerListBoxViewModel_Accessor
<T> target = new DragDropManagerListBoxViewModel_Accessor<T>(param0);
Assert.IsTrue(target.GetVisualItem(testClass)
is ListBoxItem);
}

 

 

 

 

Afin de simuler l’affichage WPF, j’ai construit une méthode qui va s’occuper de la génération des sous ensembles WPF comme si nous étions au moment de la construction d’une fenêtre WPF.

private static void PrepareUIItems(ListBox listbox)
{
IItemContainerGenerator itemContainerGenerator
= ((IItemContainerGenerator)(listbox.ItemContainerGenerator));
using (itemContainerGenerator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward, true))
{
DependencyObject cntr
= null;
do
{
cntr
= itemContainerGenerator.GenerateNext();
if (cntr != null)
itemContainerGenerator.PrepareItemContainer(cntr);
}
while (cntr != null);
}
}

Remarque 1 : Le “using … StartAt” est très important car c’est uniquement à la sortie de ce using que le ItemContainerGenerator WPF aura terminé entièrement la préparation de l’organisation de ces éléments internes.

Remarque 2: Il est impératif d’utiliser l’interface IItemContainerGenerator pour pouvoir appeler StartAt,…

En insérant cette méthode dans le code à tester, la fonction appelant du composant WPF peut être testée et le test unitaire fonctionne maintenant correctement.

public void GetVisualItemTestHelper<T>()
where T : class, ICloneable
{
ListBox listbox
= new ListBox();
T testClass
= new Object() as T;
DragDropManagerListBoxViewModel
<T> dragdropManagerListBoxViewModel = new DragDropManagerListBoxViewModel<T>(listbox);
PrivateObject param0
= new PrivateObject(dragdropManagerListBoxViewModel);
DragDropManagerListBoxViewModel_Accessor
<T> target = new DragDropManagerListBoxViewModel_Accessor<T>(param0);
Assert.IsNull((target.GetVisualItem(
0) as ListBoxItem));

ObservableCollection
<T> collection = new ObservableCollection<T>();
collection.Add(testClass);
listbox.ItemsSource
= collection;
// Generate WPF UI Items
PrepareUIItems(listbox);
Assert.IsTrue(target.GetVisualItem(
0) is ListBoxItem);
Assert.IsNull((target.GetVisualItem(
1) as ListBoxItem));
}

[TestMethod()]
[DeploymentItem(
"DragDropManager.dll")]
public void GetVisualItemTest()
{
GetVisualItemTestHelper
<GenericParameterHelper>();
}

Remarque : Le helper créé avec l’assistant de test unitaire Visual Studio nous simplifie la tâche et nous permet de ne pas devoir créer une classe spécifique : le test peut ainsi être effectué avec le Generic .

Conclusion

Nous venons de voir comment implémenter le DragAndDrop suivant le modèle MVVM pour une ListView ou ListBox.

La Multi-Sélection est présent, la sélection et la destination des éléments est améliorée avec des Adorners, la copie ainsi que le déplacement des éléments sont gérés et le scroll est automatique avec le déplacement de la souris sur les bords.

Dans les prochaines parties, nous verrons comment gérer ces mêmes fonctionnalités avec un look TreeView.

Voici le lien pour télécharger la solution avec le code source du projet et les tests unitaires

download32x32 wpfListView03.zip [174 ko]

 

Last Updated on Friday, 15 April 2011 22:32
 

WPF, ListView d’images, MVVM et Drag and Drop… – Partie 2

E-mail Print PDF
User Rating: / 82
PoorBest 
There are no translations available.

Introduction

Vu précédemment

Nous avons vu dans la première partie comment implémenter une liste de fichiers en WPF avec le pattern M-V-VM. Nous allons voir dans cette partie comment ajouter les vignettes images “Thumbnails” représentant les images miniatures de ces fichiers.

alt

L’exemple utilisé reprend l’application précédente (sans les vignettes) qui sera complétée au fur et à mesure.

Pour revoir la première partie :

WPF, ListView d’images, MVVM et Drag and Drop… – Partie 1

Ajout des vignettes

Le premier réflex est de modifier la partie View-Modèle pour exposer la lecture du fichier image. Nous avons besoin du nom de fichier complet avec le chemin sur le disque dur. Nous ajoutons pour cela une nouvelle classe ImageFileViewModel qui va hériter de FileViewModel. Cette classe va s’occuper uniquement de l’affichage des vignettes.

Ajout de ImageFileViewModel.cs dans le ViewModel

Il nous faut ensuite ajouter la propriété FileName dans la classe ImageFileViewModel. La propriété va renvoyer le nom complet de l’objet ImageFile contenu dans _imageFile.

ImageFileCollectionViewModel retourne désormais une collection d’objet ImageFileViewModel.

 

public string FileName
{
get { return _imageFile.FileName; }
}

 

Puis dans la partie View, nous ajoutons l’image dans le UserControl ImageFileListView.xaml

Ajout de l'image dans ImageFileListView

Le Binding est fait sur la propriété Source de l’Image avec {Path=FileName}

<ListView SelectionMode="Extended"  x:Name="ListViewImage"
ItemsSource
="{Binding Path=AllImages}" Margin="0,20,0,0">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=FileName}" Width="25" Height="25"/>
<TextBlock Text="{Binding Path=ShortName}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate> 
</
ListView>

Nous pouvons lancer l’application qui fonctionne…

Premier résultat catastrophique

et pleurer… SmileyCry parce que c’est très Lennnnnt…

Ce qu’il faut améliorer…

Le résultat est là mais l’interface est inutilisable avec un grand nombre d’images (Encore moins utilisable si leur poids est important et si le support est lent comme une clé USB par exemple) :

  1. Le chargement prend du temps car WPF essaie d’afficher toutes les vignettes en même temps. Nous souhaitons un affichage des vignettes qui va apporter un Plus à l’utilisateur mais surtout conserver une interface fluide.
  2. Chaque changement avec la ScrollBar va entrainer un gèle “Freeze” de l’application en attendant que toutes les vignettes soient lues et chargées intégralement en mémoire.
  3. Chaque changement avec la ScrollBar va entrainer le chargement en mémoire de l’image pour son traitement et provoquer ensuite un engorgement en mémoire sans libération avant la fermeture de l’application.
  4. Certaines images jpg, précédemment transformées avec un outils annexe en images verticales ne sont pas visibles. Mal reconnus avec WPF?

Multi-Threading

Présentation

La première étape passe par le multi-Thread. Pendant l’affichage des vignettes, l’utilisateur doit continuer à pouvoir utiliser l’application sans être bloqué. Nous allons donc procéder à la lecture des images dans un thread en dehors du thread WPF.

  1. Nous allons créer une propriété Thumbnail (vignette) sur l’objet ImageFileViewModel qui s’occupera de ce mécanisme et qui retournera une ImageSource prête à être utilisée par l’interface.  La classe ImageSource est suffisamment générique pour pouvoir être ensuite utilisée par n’importe quelle méthode WPF.
  2. Lors de la lecture de cette propriété, l’objet ImageFileViewModel sera ajouté à une liste “_listThumbnail” partagée entre tous les objets ImageFileViewModel (propriété déclarée en static) .
  3. Un Thread tournera en tâche de fond en désynchronisé et bouclera sur la liste (pile) en dépilant de la liste les images traitées et redimensionnées en thumbnails.
  4. Un message de notification WPF sera ensuite envoyé à la vue pour lui demander de rafraichir l’image Thumbnail prête à être affichée.

Voici un petit schéma pour essayer de résumer tout cela.

Multi Threading wpf UI graph

Détail

Création de la propriété Thumbnail dans l’objet .

Déclaration de la propriété Thumbnail en ImageSource afin de créer la vignette avec les mécanismes de WPF.
Dans le get,  si l’image n’est pas initialisée nous appelons LoadImage qui va empiler l’objet à la liste _listThumbnail

private static readonly List<ImageFileViewModel> _listThumbnail 
= new List<ImageFileViewModel>();
private ImageSource _thumbnail = null;
private bool _loading = false;
...
public ImageSource Thumbnail
{
get
{
if (_thumbnail == null)
{
LoadImage();
return null;
}
return _thumbnail;
}
}

internal void LoadImage()
{
if (!_loading)
{
_loading
= true;
AddPhotoToLoad(
this);
}
}

LoadImage va ajouter l’image dans la liste. _loading va permettre de savoir si la vignette est déjà chargée et éviter de refaire le traitement pour rien.

Nous sommes dans de l’appel multi-thread, afin de ne pas ajouter 2 images au même moment et de créer des conflits, nous devons verrouiller le mécanisme au moment de l’ajout et notifier aux autres Thread que l’ajout a bien été effectué.

Le verrouillage se fait avec le lock d’un objet static partagé et l’objet _loadThumbImageEvent va nous permettre de notifier un changement entre les Threads à travers un événement.

private static readonly object _lockListThumbnail = new object();
private static AutoResetEvent _loadThumbImageEvent = new AutoResetEvent(false);
...

private static void AddPhotoToLoad(ImageFileViewModel p)
{
lock (_lockListThumbnail)
{
_listThumbnail.Add(p);
}
_loadThumbImageEvent.Set();
}

Nous allons maintenant traiter les images empilées dans _listThumbnail de façon désynchronisée. Nous avons besoin maintenant du Thread qui va boucler et traiter les éléments présents dans la liste _listThumbnail lorsque le processeur aura le temps.

La déclaration et la création du Thread ne se fait qu’une seule fois dans le constructeur static.

Le Thread s’exécute en fond de tache (IsBackground = true) afin de permettre la fermeture de l’application sans attendre la fin du Thread et en priorité basse (ThreadPriority.BelowNormal) pour ne pas pénaliser l’interface. Au lancement, le Thread exécute la méthode static LoaderThreadLoop()

 

static ImageFileViewModel()
{
_loaderThreadThumbnail
= new Thread(new ThreadStart(LoaderThreadLoop));
_loaderThreadThumbnail.IsBackground
= true;
_loaderThreadThumbnail.Priority
= ThreadPriority.BelowNormal;
_loaderThreadThumbnail.Start();
}

 

La méthode LoaderThreadLoop boucle de façon infinie et va traiter la pile d’image avec la méthode ProcessLoadImageStack(). L’attente de 10 ms permet de redonner la main à l’autre Thread WPF et de rendre ainsi  l’application plus fluide.

_LoadThumbImageEvent.WaitOne() attend une notification par Set() envoyé lors du LoadImage.

 

private static void LoaderThreadLoop()
{
do
{
_loadThumbImageEvent.WaitOne();
while (ProcessLoadImageStack())
{
Thread.Sleep(
10);
}
}
while (true);
}

 

ProcessLoadImageStack() doit maintenant récupérer le dernier élément de la liste, l’enlever de la liste puis appeler l’instruction de lecture de l’image et de génération de la vignette  DoLoadImage().

private static bool ProcessLoadImageStack()
{
ImageFileViewModel p
= null;
if (_listThumbnail.Count > 0)
{
lock (_lockListThumbnail)
{
if (_listThumbnail.Count > 0)
{
p
= _listThumbnail[_listThumbnail.Count - 1];
_listThumbnail.Remove(p);
}
}
}
if (p != null)
p.DoLoadImage();
return true;
}

DoLoadImage() va charger l’image dans un Stream puis le traitement va être réalisé par des actions WPF.

Le chargement de l’image en Stream s’effectue dans la partie Model. Nous ajoutons le code Stream LoadImage() suivant dans l’interface

 

namespace wpfListViewSample02.Model
{
public interface IImageFile
{
...
Stream LoadImage();
}

}

 

Puis l’implémentation dans l’objet métier

 

namespace wpfListViewSample02.Model
{
public class ImageFile: IImageFile
{
...

public Stream LoadImage()
{
byte[] buffer = File.ReadAllBytes(FileName);
return new MemoryStream(buffer);
}
}
}

 

Dans DoLoadImage, nous chargeons maintenant le Stream afin d’effectuer un traitement de vignette.

Un problème se pose de nouveau, les actions WPF ne peuvent se faire que dans le thread WPF UI qui est unique. DoLoadImage est lancé par le thread asynchrone qui traite les images quand le processeur n’est pas occupé.

Pour raccrocher les actions WPF au thread WPF, nous utilisons l’instruction Dispatcher.Invoke qui représente le Thread unique UI de WPF de l’application. Le dispatcher utilisé est celui de l’application WPF repérable par Dispatcher.CurrentDispatcher.

Il prend comme paramètre la priorité du traitement (basse, pas besoin de se presser pour ce traitement les autres actions de WPF sont prioritaires), les paramètres échangés par le délégué ici un type delegate sans argument : NoArgumentDelegate puis le traitement dans le delegate. Le traitement appelle une autre instruction GenerateThumbnail(de l’objet Stream précédemment chargé) qui va transformer l’image Stream en vignette ImageSource avec WPF et retourner l’image dans la propriété Thumbnail.

A la fin du traitement, nous vidons le Stream et notifions à WPF que la propriété Thumbnail a changée (elle contient la vignette image générée) et donc que la vue doit être rafraichie dans l’interface visuelle WPF (OnPropertyChanged(“Thumbnail”)).

private delegate void NoArgumentDelegate();
protected virtual void DoLoadImage()
{
if (!_loading || this._imageFile == null) { return; }
if (this._imageFile.IsAvailable)
{
Stream mem
= null;
try
{
mem
= this._imageFile.LoadImage();
if ((!_loading))
{
mem.Dispose();
return;
}
Dispatcher.CurrentDispatcher.Invoke
(DispatcherPriority.ApplicationIdle, (NoArgumentDelegate)
delegate()
{
_thumbnail
= GeneratedThumbnail(mem);
}
);
}
finally
{
if (mem != null)
{
mem.Close();
}
_loading
= false;
this.OnPropertyChanged("Thumbnail");
}
}
}

Un dernier petit code pour montrer comment la vignette est générée par WPF. A noter que dans le cas où nous ne trouvons pas de vignette, nous retournons un image vide et surtout pas Null sinon nous monopolisons un Traitement qui ne finira jamais (if (Thumbnail==null) … LoadImage) .

protected ImageSource GeneratedThumbnail(Stream mem)
{
mem.Position
= 0;
BitmapDecoder imgDecoder
= BitmapDecoder.Create
(mem, BitmapCreateOptions.None, BitmapCacheOption.None);
if ((imgDecoder != null)
&& (imgDecoder.Frames.Count > 0)
&& (imgDecoder.Frames[0] != null)
&& (imgDecoder.Frames[0].Thumbnail != null))
return imgDecoder.Frames[0].Thumbnail;
else
return new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgr32, null);
}

Dans ImageFileListView.xaml, nous remplaçons la propriété Source {Path=FileName} de l’Image avec {Path=Thumbnai

 <Image Source="{Binding Path=Thumbnail}" Width="25" Height="25"/>

Voilà nous pouvons lancer de nouveau l’application.

Listview application with Thread

C’est déjà beaucoup plus fluide et utilisable. iconsmile . Les images mal reconnues (sans vignettes EXIF) s’affichent alors en noir.

Optimisons encore un peu…

Avec un gros volume d’images, nous avons encore plusieurs problèmes visuels :

  1. Toutes les images vignettes restent en mémoire, ce qui va représenter plusieurs centaines de Mo.
  2. Lorsque nous jouons plusieurs fois avec l’ascenseur de haut en bas, les images sont empilées sans limites et il faut attendre parfois longtemps avant d’avoir l’affichage de la vignette attendue.

Virtualizing

VirtualizingStackPanel

Le composant ListView contient une propriété importante VirtualizingStackPanel qui va nous permettre de résoudre la problématique de l’espace mémoire. En effet, il n’est pas utile de conserver l’ensemble des vignettes pour toutes les images parcourues et nous pouvons libérer les images non visibles.

Nous allons donc indiquer dans ImageListView.xaml

  1. L’utilisation de la virtualisation : VirtualizingStackPanel.IsVirtualizing=”True”
  2. L’événement qui sera exécuté lors du recyclage des objets VirtualizingStackPanel.CleanUpVirtualizedItem = “ListViewImage_CleanUpVirtualizedItem”

 

<ListView SelectionMode="Extended"  x:Name="ListViewImage"
ItemsSource
="{Binding Path=AllImages}" Margin="0,20,0,0"
VirtualizingStackPanel.IsVirtualizing
="True"
VirtualizingStackPanel.CleanUpVirtualizedItem
="ListViewImage_CleanUpVirtualizedItem">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=Thumbnail}" Width="25" Height="25"/>
<TextBlock Text="{Binding Path=ShortName}" />
</StackPanel>
</DataTemplate> 
</ListView.ItemTemplate>
</ListView>

Dans le code de la vue nous appelons la libération de l’élément visuel de l’objet ViewModel.

if (e.Value is ImageFileViewModel)
{
(e.Value
as ImageFileViewModel).CleanUp();
}

Puis dans la partie ViewModel dans l’objet ImageFileViewModel nous implémentons CleanUp().

  1. Comme la liste est partagée dans un thread indépendant, nous validons son accès avec lock auparavant.
  2. Dans le cas où la vignette doit être supprimée car non visible, il est inutile de le traiter dans la liste. Nous l’enlevons de la liste,
  3. Nous supprimons ensuite la vignette et indiquons que le chargement n’est pas effectué (_loading = false)
    internal void CleanUp()
    {
    lock (_lockListThumbnail)
    {
    _listThumbnail.Remove(
    this);
    _loading
    = false;
    _thumbnail
    = null;
    }
    }

Pour vérifier combien d’éléments sont réellement chargés dans l’interface, je vous propose d’ajouter un bouton sur l’interface.

La méthode GetVisualCount parcourt l’affichage d’un objet et compte le nombre d’objets visuels enfants WPF correspondant au type .

private void BtnVirtualizeCount_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(
GetVisualCount
<ImageFileViewModel>(this.listView1.ListViewImage).ToString()
,
"");
}

private static int GetVisualCount<T>(DependencyObject visual) where T : class
{
int visualCount = 0;
if (visual is ContentControl
&& (visual as ContentControl).Content is T) visualCount++;
int childCount = VisualTreeHelper.GetChildrenCount(visual);
for (int i = 0; i < childCount; i++)
{
DependencyObject childVisual
= VisualTreeHelper.GetChild(visual, i);
visualCount
+= GetVisualCount<T>(childVisual);
}
return visualCount;
}

Voici le résultat, montrant que le nombre d’objets visuels affichés correspond au nombre d’images affichées dans l’interface. Le calcul des vignettes se fait à chaque fois, ainsi si l’on descend à la fin de la liste et que l’on revient au début, le calcul s’effectuera une deuxième fois pour les vignettes.

Remarque : Si le calcul est très long, il est encore possible de gérer une liste limitée d’images dans une structure de type collection servant alors de cache (voir dans la suite - partie 3).

Listview application with Thread and Virtualizing

Encore un peu plus rapide

Depuis WPF 3.5 SP1, un autre mode de Virtualization existe pour réutiliser les éléments WPF existants et éviter de créer puis détruire à chaque fois les objets virtualisés. Il en résulte une grande amélioration de la vélocité d’affichage pour les éléments WPF virtualisés. Dans l’interface WPF ImageListView.xaml, nous ajoutons la propriété VirtualizingStackPanel.VirtualizationMode = “Recycling” (qui est par défaut = "Standard").

<ListView SelectionMode="Extended"  x:Name="ListViewImage"
ItemsSource
="{Binding Path=AllImages}" Margin="0,20,0,0"
VirtualizingStackPanel.IsVirtualizing
="True"
VirtualizingStackPanel.VirtualizationMode
= "Recycling"
VirtualizingStackPanel.CleanUpVirtualizedItem
="ListViewImage_CleanUpVirtualizedItem">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=Thumbnail}" Width="25" Height="25"/>
<TextBlock Text="{Binding Path=ShortName}" />
</StackPanel>  
</
DataTemplate>
</ListView.ItemTemplate>
</ListView>

En faisant l’essai vous constaterez encore une amélioration de la rapidité non négligeable.

La gestion des vignettes sans données EXIF avec WPF

TransformedBitmap

Il nous reste à gérer  le cas où la vignette n’existe pas dans l’information EXIF de l’image. Pour cela nous modifions un peu GeneratedThumbnail(Stream mem).

GeneratedThumbnail essaie de trouver la vignette stockée ( imgDecoder.Frames[0].Thumbnail) dans le cas où on ne trouve rien, une exception est lancée et tempThumbnail est null.

Dans le cas ou frame (ImgDecoder.Frames[0] est vide, on appelle TransformedBitmap qui retaille l’image trouvée et retourne une ImageSource avec ScaleTransform. L’image retournée fait ici une hauteur ou une largeur maximale de 240 pixels.

Dans le dernier cas où l’image n’est toujours pas reconnue nous retournons le bitmap noir.

private ImageSource GeneratedThumbnail(Stream mem)
{
ImageSource tempThumbnail
= null;
BitmapFrame frame
= null;
try
{
mem.Position
= 0;
BitmapDecoder imgDecoder
= BitmapDecoder.Create
(mem, BitmapCreateOptions.None, BitmapCacheOption.None);
if (imgDecoder != null && imgDecoder.Frames.Count > 0)
frame
= imgDecoder.Frames[0];
if (frame != null)
tempThumbnail
= frame.Thumbnail;
}
catch
{
tempThumbnail
= null;
}

if (tempThumbnail == null && frame != null)
tempThumbnail
= TransformedThumbnail(frame);
if (tempThumbnail != null)
return tempThumbnail;
else
return new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgr32, null);
}

private ImageSource TransformedThumbnail(BitmapFrame frame)
{
TransformedBitmap tempThumbnail
= new TransformedBitmap();
tempThumbnail.BeginInit();
try
{
tempThumbnail.Source
= frame as BitmapSource;
int decodeH = 240;
int decodeW = 240;
int pixelH = frame.PixelHeight;
int pixelW = frame.PixelWidth;
if (pixelH / pixelW > 0 && pixelH > decodeH)
decodeW
= (frame.PixelWidth * decodeH) / pixelH;
else
if (pixelW > decodeW)
decodeH
= (frame.PixelHeight * decodeW) / pixelW;
double scaleX = decodeW / (double)pixelW;
double scaleY = decodeH / (double)pixelH;
TransformGroup transformGroup
= new TransformGroup();
transformGroup.Children.Add(
new ScaleTransform(scaleX, scaleY));
tempThumbnail.Transform
= transformGroup;
}
finally
{
tempThumbnail.EndInit();
}
WriteableBitmap writable
= new WriteableBitmap(tempThumbnail);
return writable;
}

Objets Freezable et Freeze() et animation d’attente

Problématique

Le traitement précédent est assez lourd et peut occuper du temps processeur dans le cas de transformation en vignettes d’images de taille importante.

Afin d’optimiser toujours un peu plus l’interface, il serait intéressant de ne pas monopoliser le thread UI WPF pour réaliser cette manipulation. Nous allons pour cela utiliser la méthode Freeze() des objets WPF dérivant de l’objet Freezable.

Implémentation de Freeze()

Cette méthode va permettre un accès en lecture seule à WPF au objets que nous manipulons dans un autre Thread.

Dans la précédente méthode DoLoadImage, La transformation GeneratedThumbnail était effectuée dans le delegate() (Dans le thread WPF UI).

Nous le déplaçons dans le Thread de chargement précédemment mis en place et utilisons un objet intermédiaire “tempImgSrc”. “tempImgSrc.Freeze()” permet de geler l’objet en écriture pour WPF et de le rendre accessible en lecture au Thread WPF.

Nous affectons ensuite tempImgSrc à la propriété Thumbnail dans le Thread WPF UI.

protected virtual void DoLoadImage()
{
if (!_loading || this._imageFile == null) { return; }
if (this._imageFile.IsAvailable)
{
Stream mem
= null;
try
{
mem
= this._imageFile.LoadImage();
if ((!_loading))
{
mem.Dispose();
return;
}
ImageSource tempImgSrc
= GeneratedThumbnail(mem);
tempImgSrc.Freeze();
Dispatcher.CurrentDispatcher.Invoke
(DispatcherPriority.ApplicationIdle, (NoArgumentDelegate)
delegate()
{
// old _thumbnail = GeneratedThumbnail(mem);
_thumbnail = tempImgSrc;
});
}
finally
{
if (mem != null)
{
mem.Close();
}
_loading
= false;
this.OnPropertyChanged("Thumbnail");
}
}
}

Le schéma général ressemble à cela maintenant :

 

WpfDragDrop06b

Animation d’attente

Dernière étape, afficher une petite animation d’attente lors de la génération des vignettes qui peut parfois prendre un peu de temps.

Afin de faciliter cette manipulation, nous allons ajouter une propriété IsLoaded dans la classe ImageFileViewModel de la couche ViewModel.

public bool IsLoaded
{
get
{
return (_thumbnail!=null);
}
}

Cette propriété retourne true si l’image est présente et false si l’image n’est pas encore chargée.

Dans le DoLoadImage, nous ajoutons une notification à WPF du changement de IsLoaded lorsque l’image est chargée :

protected virtual void DoLoadImage()
{
if (!_loading || this._imageFile == null) { return; }
if (this._imageFile.IsAvailable)
{
Stream mem
= null;
try
...
finally
{
if (mem != null)
{
mem.Close();
}
_loading
= false;
OnPropertyChanged(
"Thumbnail");
OnPropertyChanged(
"IsLoaded");
}
}
}

 

Dans la partie View, nous allons afficher un sablier avec une petite animation lorsque l’image est en cours de chargement.

<StackPanel Orientation="Horizontal" Height="25">
<Image x:Name="ThumbnailImage"
Visibility
="Collapsed"
Height
="20" Width="20"
Source
="{Binding Path=Thumbnail}">
<Image.BitmapEffect>
<DropShadowBitmapEffect ShadowDepth="2" />
<
/Image.BitmapEffect>
</Image>
<Image x:Name="WaitingImage"
Visibility
="Visible"
Height
="20" Width="20"
Source
="/View/Hourglass.png">

</
Image>
<TextBlock Text="{Binding Path=ShortName}" />
</StackPanel>

Deux images “ThumbnailImage” et “WaitingImage” sont affichées ou cachées en fonction de la propriété “IsLoaded”

<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsLoaded}" Value="True">
<Setter Property="Visibility" TargetName="ThumbnailImage" Value="Visible"/>
<Setter Property="Visibility" TargetName="WaitingImage" Value="Collapsed"/>
</DataTrigger>

<DataTrigger Binding="{Binding Path=IsLoaded}" Value="False">
<Setter Property="Visibility" TargetName="WaitingImage" Value="Visible"/>
<Setter Property="Visibility" TargetName="ThumbnailImage" Value="Collapsed"/>

</
DataTrigger>

Pour afficher le sablier avec une petite animation, nous ajoutons une ressource Storyboard “WaitingTimeLine” et un déclenchement de cette animation avec un arrêt de l’animation lors du changement d’état de la valeur IsLoaded.

DataTrigger.EnterActions permet de déclencher l’animation
DataTrigger.ExitActions permet d’arrêter l’action précédemment définie

<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsLoaded}" Value="True">
<Setter Property="Visibility" TargetName="ThumbnailImage" Value="Visible"/>
<Setter Property="Visibility" TargetName="WaitingImage" Value="Collapsed"/>
</
DataTrigger>

<DataTrigger Binding="{Binding Path=IsLoaded}" Value="False">
<Setter Property="Visibility" TargetName="WaitingImage" Value="Visible"/>
<Setter Property="Visibility" TargetName="ThumbnailImage" Value="Collapsed"/>

<DataTrigger.EnterActions>
<BeginStoryboard x:Name="WaitingTimeline_BeginStoryboard"
Storyboard
="{StaticResource WaitingTimeline}"/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="WaitingTimeline_BeginStoryboard"/>

</
DataTrigger.ExitActions>
</
DataTrigger> 
</
DataTemplate.Triggers>

L’animation Storyboard “WaitingTimeLine” est définie ainsi :

<DataTemplate.Resources>
<Storyboard x:Key="WaitingTimeline" Timeline.DesiredFrameRate="10">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" RepeatBehavior="Forever"
Storyboard.TargetName
="WaitingImage"
Storyboard.TargetProperty
="(UIElement.RenderTransform).(TransformGroup.Children)[0]
.(RotateTransform.Angle)"
>
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="-15"/>
<SplineDoubleKeyFrame KeyTime="00:00:03" Value="15"/>
</
DoubleAnimationUsingKeyFrames>
</
Storyboard>
</DataTemplate.Resources>

l’animation chargée en ressource va s’appliquer à la transformation "rotation” de l’image “WaitingImage”.

Ce Storyboard fait varier un nombre de –15 à +15 selon une chronologie définie. Ce nombre correspondra à une inclinaison en degré de la rotation du sablier.

Encore une petite astuce d’optimisation, les animations en WPF sont paramétrées par défaut pour générer 60 fps (frame per seconds) , ce qui est complètement inutile dans notre cas. Nous pouvons accepter une animation à 10 images par secondes, nous ajoutons donc la propriété TimeLine.DesiredFrameRate=”10”. Cela ne changera rien à l’affichage et libérera encore un peu de puissance CPU / GPU (processeur principal ou de la carte graphique).

 

<StackPanel Orientation="Horizontal" Height="25">
<Image x:Name="ThumbnailImage"
Visibility
="Collapsed"
Height
="20" Width="20"
Source
="{Binding Path=Thumbnail}">
<Image.BitmapEffect>
<DropShadowBitmapEffect ShadowDepth="2" />
</
Image.BitmapEffect>
</Image>
<Image x:Name="WaitingImage"
Visibility
="Visible"
Height
="20" Width="20"
Source
="/View/Hourglass.png">
<Image.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0" CenterX="10" CenterY="10"/>
</TransformGroup>
</Image.RenderTransform>
</Image>
<TextBlock Text="{Binding Path=ShortName}" />

</
StackPanel>

Exécutons l’application

Application Listview with waiting animation

L’interface utilisateur est fluide et l’application consomme une charge mémoire très raisonnable, même avec un nombre très important d’images.

Smileylol

Les tests unitaires

Et les tests unitaires alors ?

Je le répète une nouvelle fois pour bien insister, toute l’organisation M-V-VM, comme toute organisation de projet en couche (MVC, MVP,…) a comme principal objectif de pouvoir automatiser les tests de l’application et donc de faire gagner beaucoup de temps sur la phase finale du développement et sur les maintenances.

Premier problème : La classe ImageFileViewModel utilise un constructeur static pour gérer la liste des Thumbnails.

Comment faire des tests unitaires avec un constructeur static et comment le réinitialiser ?

Sans réinitialisation, les tests unitaires qui s’exécutent les uns à la suite des autres vont se polluer et générer des cas d’erreurs là où il ne devrait pas y en avoir ou pire, s’exécuter avec succès alors qu’ils ne devraient chuter. Le seul moyen est de réinitialiser l’objet à chaque test, sauf que ici, petit problème… la classe à un constructeur static, donc construit à la première rencontre de cette classe par le premier test unitaire.

Un moyen rencontré est de réinitialiser les attributs static que les objets utilisent et d’utiliser la réflexion pour rappeler le constructeur static.

Avec l’attribut “[TestInitialize]”, nous nous assurons de réinitialiser un état propre à chaque début de tous les tests.

le ci.Invoke permet d’appeler le constructeur static récupéré par réflexion avec typeof puis TypeInitializer.

ImageFileViewModel_Accessor est un objet permettant d’accéder aux attributs privés de l’objet ImageFileViewModel. Grâce à cet Accessor, nous pouvons réinitialiser la liste des Thumbnails en la vidant.

[TestInitialize()]
public void MyTestInitialize()
{
// call the static constructor and clear static attribute _ListThumbnails
Type staticType = typeof(ImageFileViewModel);
ConstructorInfo ci
= staticType.TypeInitializer;
object[] parameters = new object[0];
ci.Invoke(
null, parameters);
ImageFileViewModel_Accessor._listThumbnail.Clear();
}

Avec ce mécanisme chaque objet de type ImageFileViewModel est remis à vide avant de commencer un nouveau test.

Un autre problème va concerner les tests unitaires sur les comportements multi-Thread avec WPF.

Comment tester les comportements Multi-Thread WPF avec des tests unitaires?

Le problème se pose pour tester la propriété “Thumbnail”.

Je rappelle ici le code à tester

public ImageSource Thumbnail
{
get
{
if (_thumbnail == null)
{
LoadImage();
return null;
}
return _thumbnail;
}
}

Nous devons tester, après la fin du traitement dans le thread, que la liste des _listThumbnail est vide et que la propriété Thumbnail n’est pas null.

Pour attendre le déroulement d’un Dispatcher, la classe DispatcherFrame va venir à notre secours.

La classe DispatcherFrame

DispatcherFrame avec le Dispatcher possèdent une action d’attente du Dispatcher.CurrentDispatcher

Dispatcher.PushFrame(frame)”  va attendre jusqu’à ce que l’objet frame de type DispatcherFrame initialise la propriété “Continue = false”.

Continue = false dans notre cas devrait se dérouler lorsque la propriété Thumbnail indique à WPF qu’elle contient une information et que la View WPF doit se rafraichir, c’est à dire au moment ou “OnPropertyChanged("Thumbnail");” est appelé par une des méthodes lancées à la suite de LoadImage.

PropertyChangedEventHandler waitForModelHandler = new PropertyChangedEventHandler(
delegate(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Thumbnail")
{
frame.Continue
= false;
}
});
tempimageFileViewModel.PropertyChanged
+= waitForModelHandler;

Pour ajouter frame.Continue à ce moment, nous utilisons un EventHandler de changement de propriété PropertyChangedEventHandler et nous initialisons frame.Continue = false si la propriété “Thumbnail” est notifiée d’un changement.

Le test unitaire ressemble à ça maintenant.

Nous utilisons un fichier jpg pour initialiser le fichier image à transformer en vignette.

[TestMethod()]
[DeploymentItem(
@"test01.jpg")]
public void ThumbnailTest()
{
IImageFile imageFile
= new ImageFile();
(imageFile
as ImageFile).FileName
= Path.Combine(Directory.GetCurrentDirectory(), @"test01.jpg");

ImageFileViewModel tempimageFileViewModel
= new ImageFileViewModel(imageFile);
PrivateObject param0
= new PrivateObject(tempimageFileViewModel);
ImageFileViewModel_Accessor imageFileViewModel
= new ImageFileViewModel_Accessor(param0);
// check no items in _listThumbnail
Assert.AreEqual(0, ImageFileViewModel_Accessor._listThumbnail.Count);
// Implement DispatcherFrame for waiting and testing Dispatcher WPF process
DispatcherFrame frame = new DispatcherFrame();
PropertyChangedEventHandler waitForModelHandler
= new PropertyChangedEventHandler(
delegate(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Thumbnail")
{
frame.Continue
= false;
}
});
tempimageFileViewModel.PropertyChanged
+= waitForModelHandler;
// Launch Thumbnail process
var thumb = tempimageFileViewModel.Thumbnail;
// Waiting frame.continue=false
Dispatcher.PushFrame(frame);
// Check thumbnail not null
Assert.AreNotEqual(imageFileViewModel._thumbnail, null);
// Check _listThumbnail empty
Assert.AreEqual(0, ImageFileViewModel_Accessor._listThumbnail.Count);
}

Notez l’attribut “[DeploymentItem(@"test01.jpg")]“qui permet d’utiliser le fichier copié test01.jpg au moment du test unitaire. Pensez également , dans ce cas, à modifier la propriété du fichier dans la solution Visual Studio pour indiquer de toujours copier ce fichier dans le répertoire de destination.

Exemple des tests unitaires

Il ne reste plus qu’à exécuter l’ensemble des tests pour terminer

Unit test results

Tout fonctionne à merveille

Smileylol

Conclusion

Nous venons de voir comment implémenter une ListView avec une interface multi-Thread en WPF, la gestion des images et des vignettes EXIF, la virtualisation et les mécanismes d’attente en WPF.

Tout cela dans une implémentation par couche M-V-VM qui nous permet de réaliser les tests unitaires sans se soucier de la partie visible XAML/WPF.

Dans les prochaines parties, nous verrons comment gérer les images en cache, le Drag & Drop avec notre ListView et comment lui apporter un look TreeView.

Voici le lien pour télécharger la solution avec le code source du projet et les tests unitaires

download32x32 wpfListView02.zip

et le lien pour juste observer le résultat avec ce petit exemple

download32x32 wpfListViewSample02.exe

Last Updated on Friday, 14 May 2010 21:55
 

WPF, ListView d’images, MVVM et Drag and Drop… – Partie 1

E-mail Print PDF
User Rating: / 72
PoorBest 
There are no translations available.

Introduction

Pour les besoins d’un logiciel orienté « Photos », j’ai eu besoin des fonctionnalités essentielles suivantes :

  • Affichage des images sous forme de vignettes dans une liste et dans une arborescence de type TreeView
  • Cet affichage doit permettre de gérer un nombre imposant de photos (>5000 !) sans générer de débordement mémoire ou de ralentissement
  • L’affichage et la génération des vignettes doit pouvoir se faire en mode asynchrone (en lisant les informations EXIF si elles existent,…)
  • La multi-sélection doit permettre de sélectionner plusieurs éléments.
  • Les éléments sélectionnés doivent pouvoir être déplacés ou copiés dans la liste (ou l’arbre) par Drag And Drop

Et le tout en DotNet WPF respectant le pattern MVVM !!!

MVVM Pattern

Je ne ferai pas un gros article sur la nécessité de développer en WPF selon une architecture MVVM (Séparation des fonctions, réutilisation, rôle des objets mieux ciblés, accès déclaratifs…). Voici quelques liens au cas où vous ne seriez toujours pas convaincus…

http://www.orbifold.net/default/?p=550
http://japf.developpez.com/tutoriels/dotnet/mvvm-pour-des-applications-wpf-bien-architecturees-et-testables/
http://msdn.microsoft.com/fr-fr/magazine/dd419663.aspx
http://www.c2i.fr/Article/Detail/a3809f7b-196a-4d8c-bb48-164f591920bb

MVVM et ListView

Cette première partie présente l’implémentation de la liste d’images avec le modèle MVVM.

Exemple d’application

Voici le descriptif fonctionnel de l’exemple que nous allons mettre en place : Un petit croquis vaut mieux qu’un long discours.

Draft MVVM application

L’application est extrêmement simpliste. La saisie d’un répertoire se fait en haut, le click sur le bouton affiche la liste des images et leur nombre total.

Découpage de l’application

L’application est découpée en 4 parties :

  • Model : l’objet “métier”image
  • ViewModel : les objets nécessaire à la manipulation des objets “Model” images par la couche “View” (Détail de l’image et liste des images)
  • View : L’affichage XAML-WPF de la liste des images
  • Controler : Les actions nécessaires : Remplissage de la liste par exemple

Avec un diagramme, cela ressemble à ceci :

MVVM Pattern

L’organisation des classes dans la solution se traduit comme ceci

MVVM solution explorer

 

Détail des couches MVVM

La couche Model

Cette couche n’est pas spécifique à la technologie WPF.
J’ai ajouté une interface IImageFile pour simplifier les évolutions.
Voici l’interface de manipulation de l’objet Image.

public interface IImageFile
{
string FileName { get; }
bool IsAvailable { get; }
}

FileName contient le chemin complet du fichier Image
IsAvailable retourne vrai si le fichier existe

Et voici son implémentation

public class ImageFile: IImageFile
{
public string FileName { get; set; }
public bool IsAvailable
{
get
{
return File.Exists(FileName);
}
}
}

 

 

La couche ViewModel

Passons maintenant à la couche ViewModel qui va exposer les informations nécessaires à la vue.

  • La classe FileViewModel va contenir l’objet ViewModel pour un fichier image
  • La classe ImageFileCollectionViewModel va contenir la liste des objets FileViewModel

Pour se faire les classes implémentent l’interface INotifiedProperty.
Cette interface nécessite l’implémentation d’un événement de type PropertyChangedEventHandler qui remontra

la classe FileViewModel

La propriété ShortName permettra d’afficher uniquement le nom du fichier, l’objet métier complet sera stocké dans un attribut privé _ImageFile.

public class FileViewModel: INotifyPropertyChanged
{
protected IImageFile _imageFile;
public string ShortName
{
get { return Path.GetFileName(_imageFile.FileName); }
}

public FileViewModel(IImageFile imageFile)
{
this._imageFile = imageFile;
}

public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(
this, new PropertyChangedEventArgs(propertyName));
}
}
}

La classe ImageFileCollectionViewModel

La liste des fichiers images sera exposée à travers une autre classe ImageFileCollectionViewModel.
Pour obtenir la liste des images dans la vue nous utiliserons donc la propriété AllImages de type ObservableCollection de FileViewModel.

DataItemCount permettra d’obtenir le nombre total d’images dans la liste. DataItemCount  appelle le fameux OnPropertyChanged qui permettra à chaque modification du nombre d’éléments (de la couche Model)  de notifier à la vue XAML de se rafraichir automatiquement.

L’ajout d’éléments dans la liste se fera avec la méthode AddNewPhotoItem.

public class ImageFileCollectionViewModel: INotifyPropertyChanged
{
private ObservableCollection<FileViewModel> _allImages;
private int dataItemsCount;

public ObservableCollection<FileViewModel> AllImages
{
get { return _allImages; }
}

public int DataItemsCount
{
get
{
return dataItemsCount;
}
private set
{
dataItemsCount
= value;
OnPropertyChanged(
"DataItemsCount");
}
}

public ImageFileCollectionViewModel()
{
this._allImages = new ObservableCollection<FileViewModel>();
this.DataItemsCount = 0;
}

public void AddNewPhotoItem(IImageFile imageFile)
{
FileViewModel newImageFileViewModel
= new FileViewModel(imageFile);
this._allImages.Add(newImageFileViewModel);
this.DataItemsCount++;
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(
this, new PropertyChangedEventArgs(propertyName));
}
}
}

La couche View

L’affichage de la liste se fait ensuite avec une vue XAML classique. Pour améliorer la réutilisation et faciliter la maintenance j’ai créé un UserControl « ImageFileListView » qui contient uniquement la liste des images.

Voici l’implémentation de ce UserControl. Notez que le Binding se fait sur les propriétés des objets ViewModel

<UserControl x:Class="wpfListViewSample01.View.ImageFileListView"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local
="clr-namespace:wpfListViewSample01.View">

<ListView SelectionMode="Extended" x:Name="ListViewImage"
ItemsSource
="{Binding Path=AllImages}" Margin="0,20,0,0">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=ShortName}" />
 </StackPanel >
</DataTemplate  >
</ListView.ItemTemplate>   
</ListView >
 
</UserControl >

Ce UserControl est appelé de la façon suivante dans la fenêtre principale Window.xaml.
Notez que “label2” est bindé sur la propriété DataItemsCount de l’objet ViewModel

 

<Window x:Class="wpfListViewSample01.View.Window1"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m
="clr-namespace:wpfListViewSample01.View"
Title
="Window1" Height="400" Width="571">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="515*" />
<ColumnDefinition Width="34*" />
</Grid.ColumnDefinitions>
<Label Height="25" HorizontalAlignment="Left" Margin="12,17,0,0"
Name
="label1" VerticalAlignment="Top" Width="108">Images Directory
</Label >
<TextBox Height="25" Margin="126,17,12,0" Name="textBoxImageDirectory"
VerticalAlignment
="Top" Grid.ColumnSpan="2" />
<Button Height="29" HorizontalAlignment="Left" Margin="12,56,0,0"
Name
="BtnOk" VerticalAlignment="Top" Width="106" Click="BtnOk_Click">
Display!
</Button  >
<m:ImageFileListView x:Name="listView1" Margin="12,99,12,22"
ClipToBounds
="False" Grid.ColumnSpan="2" />
<Label Height="24" HorizontalAlignment="Right" Margin="0,57,72,0"
Name
="label2" VerticalAlignment="Top" Width="170"
Content
="{Binding Path=DataItemsCount}">
</Label>
</Grid   >
</Window >

 

Les liens entre Model, ViewModel et View

Nous venons de voir que la couche View et ViewModel sont directement reliées par le Binding des fichiers XAML.
Au moment du click sur le bouton, l’alimentation de la liste doit se faire avec le répertoire saisi. 
Sur le click du bouton, j’ai codé quelques actions.

  1. Création de l’objet ViewModel ImageFileCollectionViewModel qui va contenir l’ensemble des images.
  2. Alimentation de la liste des fichiers images présents dans le répertoire textBoxImageDirectory.Text.
  3. Initialisation des View avec la propriété DataContext de chaque composant visuel.

Voici une écriture possible.

 

private void BtnOk_Click(object sender, RoutedEventArgs e)
{
ImageFileCollectionViewModel ImagesViewModel
= new ImageFileCollectionViewModel();
ImageFileControler.CompleteViewList(ImagesViewModel, textBoxImageDirectory.Text);
listView1.DataContext
= ImagesViewModel;
label2.DataContext
= ImagesViewModel;
};

 

 

ImageFileControler.CompleteViewList recherche les fichiers *.jp* du répertoire passé en paramètre et les aoute un par un dans la liste de type ImageFileCollectionViewModel.

 

public static void CompleteViewList(
ImageFileCollectionViewModel imageFileCollecion,
string directory)
{
string[] files = Directory.GetFiles(directory, "*.jp*");
foreach (var f in files)
{
var im
= new ImageFile();
im.FileName
= f;
imageFileCollecion.AddNewPhotoItem(im);
}
}

 

Exécution de l’application

L’application est loin d’être terminée mais voici une première exécution.

WPF MVVM Application example

Les tests unitaires

Et les tests alors?
Normalement les tests unitaires sont fait en même temps, voir même avant l’écriture des différentes implémentations. J’ai préféré poser le décor avant d’aborder cette partie qui a bien sur été réalisée avant l’implémentation des différentes méthodes .
Le découpage MVVM va nous faciliter la tâche en terme de tests unitaires.

Trois tests unitaires ont été ajoutés sur la partie ViewModel. Le but est de tester les interactions entre l’affichage et les objets métiers du ImageFileCollectionViewModel.

  • DataItemsCountTest : Test de ImageFileCollectionViewModel.DataItemsCount
  • AllImagesTest : Test de ImageFileCollectionViewModel.AllImages
  • AddNewPhotoItemTest : Test de ImageFileCollectionViewModel.AddNewPhotoItem

Les tests unitaires sont créés avec l’assistant Visual Studio 2008 (Depuis ImageFileCollectionViewModel click droit, Créer des Tests unitaires... )

DataItemsCountTest ()

Il s’agit de tester le Nombre d’éléments d’objets métiers ImageFile ajoutés dans la liste.

  1. Création de l’objet ImageCollectionViewModel
  2. Test si la propriété DataItemsCount est bien = 0
  3. Ajout de 2 fichiers images Fictifs
  4. Test si la propriété DataItemsCount est bien = 2

Voici le code.

 

[TestMethod()]
public void DataItemsCountTest()
{
ImageFileCollectionViewModel target
= new ImageFileCollectionViewModel();
Assert.AreEqual(target.DataItemsCount,
0);
target.AddNewPhotoItem(
new ImageFile() { FileName = "c:\test1.jpg" });
target.AddNewPhotoItem(
new ImageFile() { FileName = "c:\test2.jpg" });
Assert.AreEqual(target.DataItemsCount,
2);
}

 

AllImagesTest()

Il s’agit de tester les éléments présents dans AllImages. La technique est la même. Attention ici AllImages représente le détail des ViewModel et non pas la liste des objets métiers.

    1. Création de l’objet ImageCollectionViewModel
    2. Test si la propriété AllImages.Count est bien = 0
    3. Ajout de 1 élément
    4. Test si la propriété AllImages.Count est bien = 1
    5. Test si le premier élément retourne bien l’affichage attendu
[TestMethod()]
public void AllImagesTest()
{
ImageFileCollectionViewModel target
= new ImageFileCollectionViewModel();
Assert.AreEqual(target.AllImages.Count,
0);
target.AddNewPhotoItem(
new ImageFile() { FileName = @"Images\Test1.jpg" });
Assert.AreEqual(target.AllImages.Count,
1);
Assert.AreEqual(target.AllImages[
0].ShortName, "Test1.jpg");

}

AddNewPhotoItemTest()

Le meilleur pour la fin. L’objectif est de tester l’ajout d’un élément. Le but n’est pas de tester cet ajout à travers AllImages mais de tester directement l’attribut privé _allImages.
Il faut donc un accès à l’attribut privé “_allImages”. La première étape à réaliser est de se placer sur l’attribut _allImages et de faire click droit puis“Créer un accesseur privé'”.

Ensuite vous pourrez utiliser un code de ce type :

  1. Création de l’objet temp de type ImageCollectionViewModel
  2. Créer un PrivateObject à partir de l’objet temp précédent
  3. Création de l’objet target à tester à partir de l’objet Accessor ImageCollectionViewModel_Accessor qui prend en paramètre l’objet privé .
  4. Ajout d’1 élément
  5. Test en utilisant _allImages.Count = 1
  6. test du type retourné par le premier élément de _allImages

 

[TestMethod()]
public void AddNewPhotoItemTest()
{
ImageFileCollectionViewModel temp
= new ImageFileCollectionViewModel();
PrivateObject param0
= new PrivateObject(temp);
ImageFileCollectionViewModel_Accessor target
= new ImageFileCollectionViewModel_Accessor(param0);
target.AddNewPhotoItem(
new ImageFile() { FileName = @"c:\test1.jpg" });
Assert.AreEqual(target._allImages.Count,
1);
Assert.AreEqual(target._allImages[
0].GetType().FullName,
"wpfListViewSample01.ViewModel.FileViewModel");
}

 

Exécution des tests

Windows Test

Magique! tout marche à merveille.

Conclusion

Nous venons de voir une implémentation possible en WPF avec le pattern MVVM d’une application affichant une liste d’images.

Nous verrons dans la prochaine étape l’affichage des vignettes, les contraintes de performances, les problèmes de gèle d’interface et la virtualisation des données.

Voici le projet complet à télécharger

download32x32 WPFListView01.zip

ou juste l’exécutable pour observer ce premier résultat

download32x32 wpfListViewSample01.exe

 

Last Updated on Friday, 14 May 2010 22:01
 

Playing with FindFirstFile and FindNextFile

E-mail Print PDF
User Rating: / 80
PoorBest 
There are no translations available.

Enumerating files in .NET is easy. Everybody knows the GetFiles method and you may be tempted to write code like this:

 

    var files = Directory.GetFiles(@"c:\windows\system32", "*.dll");
    foreach (var file in files)
    {
        Console.WriteLine(file);
    }

 

But if you look closer you will notice that this method returns an array of strings. This could be problematic if the directory you search contains lots of files. The method will block until it performs the search and once it finishes it will load all the results into memory. It would be much better if it just returned an IEnumerable. That’s exactly what the EnumerateFiles method does in .NET 4.0. Unfortunately in .NET 3.5 there’s nothing for the job.

 

In this post I will show how to implement this functionality using the FindFirstFile and FindNextFile functions.

 

We start by defining the native function prototypes:

    internal sealed class Win32Native
    {
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct WIN32_FIND_DATA
        {
            public uint dwFileAttributes;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
            public uint nFileSizeHigh;
            public uint nFileSizeLow;
            public uint dwReserved0;
            public uint dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern SafeFindHandle FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool FindNextFile(SafeFindHandle hFindFile, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool FindClose(IntPtr hFindFile);
    }

You may notice the SafeFindHandle class used in the method signatures. This is just a simple class deriving from SafeHandle that will make sure that unmanaged handle is correctly closed:

 

    [SecurityCritical]
    internal class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        [SecurityCritical]
        public SafeFindHandle() : base(true)
        { }

        [SecurityCritical]
        protected override bool ReleaseHandle()
        {
            return Win32Native.FindClose(base.handle);
        }
    }

 

As the documentation states the handle created by FindFirstFile needs to be closed with FindClose function. And finally the implementation of the EnumerateFiles method:

 

    public static class DirectoryExtensions
    {
        public static IEnumerable<string> EnumerateFiles(string path, string searchPattern)
        {
            // TODO: validate input parameters
            
            string lpFileName = Path.Combine(path, searchPattern);
            Win32Native.WIN32_FIND_DATA lpFindFileData;
            var handle = Win32Native.FindFirstFile(lpFileName, out lpFindFileData);
            if (handle.IsInvalid)
            {
                int hr = Marshal.GetLastWin32Error();
                if (hr != 2 && hr != 0x12)
                {
                    throw new Win32Exception(hr);
                }
                yield break;
            }

            if (IsFile(lpFindFileData))
            {
                var fileName = Path.Combine(path, lpFindFileData.cFileName);
                yield return fileName;
            }

            while (Win32Native.FindNextFile(handle, out lpFindFileData))
            {
                if (IsFile(lpFindFileData))
                {
                    var fileName = Path.Combine(path, lpFindFileData.cFileName);
                    yield return fileName;
                }
            }

            handle.Dispose();
        }

        private static bool IsFile(Win32Native.WIN32_FIND_DATA data)
        {
            return 0 == (data.dwFileAttributes & 0x10);
        }
    }

The method could be used like this:

    class Program
    {
        static void Main(string[] args)
        {
            var files = DirectoryExtensions.EnumerateFiles(@"c:\windows\system32", "*.dll");
            foreach (var file in files)
            {
                Console.WriteLine(file);
            }
        }
    }
Last Updated on Wednesday, 19 May 2010 21:21
 

Fun with matrix multiplication and unsafe code

E-mail Print PDF
User Rating: / 45
PoorBest 
There are no translations available.

In this post I will compare two methods that perform matrix multiplication. We start by defining the Matrix class:

    class Matrix
    {
        private readonly double[,] _matrix;
        public Matrix(int dim1, int dim2)
        {
            _matrix = new double[dim1, dim2];
        }

        public int Height { get { return _matrix.GetLength(0); } }
        public int Width { get { return _matrix.GetLength(1); } }

        public double this[int x, int y]
        {
            get { return _matrix[x, y]; }
            set { _matrix[x, y] = value; }
        }
    }

Next we add the first algorithm to the Matrix class which performs a naive multiplication:

    public static Matrix NaiveMultiplication(Matrix m1, Matrix m2)
    {
        Matrix resultMatrix = new Matrix(m1.Height, m2.Width);
        for (int i = 0; i < resultMatrix.Height; i++)
        {
            for (int j = 0; j < resultMatrix.Width; j++)
            {
                resultMatrix[i, j] = 0;
                for (int k = 0; k < m1.Width; k++)
                {
                    resultMatrix[i, j] += m1[i, k] * m2[k, j];
                }
            }
        }
        return resultMatrix;
    }

The second method uses unsafe code:

   public unsafe static Matrix UnsafeMultiplication(Matrix m1, Matrix m2)
   {
       int h = m1.Height;
       int w = m2.Width;
       int l = m1.Width;
       Matrix resultMatrix = new Matrix(h, w);
       unsafe
       {
           fixed (double* pm = resultMatrix._matrix, pm1 = m1._matrix, pm2 = m2._matrix)
           {
               int i1, i2;
               for (int i = 0; i < h; i++)
               {
                   i1 = i * l;
                   for (int j = 0; j < w; j++)
                   {
                       i2 = j;
                       double res = 0;
                       for (int k = 0; k < l; k++, i2 += w)
                       {
                           res += pm1[i1 + k] * pm2[i2];
                       }
                       pm[i * w + j] = res;
                   }
               }
           }
       }
       return resultMatrix;
   }

Now it’s time to measure the performance:

    class Program
    {
        [DllImport("kernel32.dll")]
        static extern void QueryPerformanceCounter(ref long ticks);

        static long Measure(Action action, int count)
        {
            long startTicks = 0;
            QueryPerformanceCounter(ref startTicks);
            for (int i = 0; i < count; i++)
            {
                action();
            }
            long endTicks = 0;
            QueryPerformanceCounter(ref endTicks);
            return endTicks - startTicks;
        }

        static void Main(string[] args)
        {
            Random random = new Random();

            Matrix m1 = new Matrix(20, 30);
            for (int i = 0; i < m1.Height; i++)
            {
                for (int j = 0; j < m1.Width; j++)
                {
                    m1[i, j] = random.Next(-100, 100);
                }
            }

            Matrix m2 = new Matrix(30, 40);
            for (int i = 0; i < m2.Height; i++)
            {
                for (int j = 0; j < m2.Width; j++)
                {
                    m2[i, j] = random.Next(-100, 100);
                }
            }

            Console.WriteLine(Measure(() => Matrix.NaiveMultiplication(m1, m2), 10000));
            Console.WriteLine(Measure(() => Matrix.UnsafeMultiplication(m1, m2), 10000));
        }
    }

In this test we perform 10000 multiplications of two randomly generated matrices with sizes 20x30 and 30x40 using both methods:

Method CPU cycles
unsafe multiplication 4485698
naive multiplication 58762273

The results show that the naive multiplication is slower by a factor of 13 compared to the multiplication using unsafe code and working directly with memory.

Last Updated on Saturday, 03 October 2009 12:31
 

LINQ to XML and reading large XML files

E-mail Print PDF
User Rating: / 64
PoorBest 
There are no translations available.

LINQ to XML makes it relatively easy to read and query XML files. For example consider the following XML file:

    xml version="1.0" encoding="utf-8" ?>
<users>
<user name="User1" groupid="4" />
<user name="User2" groupid="1" />
<user name="User3" groupid="3" />
<user name="User4" groupid="1" />
<user name="User5" groupid="1" />
<user name="User6" groupid="2" />
<user name="User7" groupid="1" />
</users>
 

Suppose you would like to find all records with groupid > 2. You could be tempted to issue the following query:

    XElement doc = XElement.Load("users.xml");
var users
= from u in doc.Elements("user")
where u.Attribute("groupid") != null &&
int.Parse(u.Attribute("groupid").Value) > 2
select u;
Console.WriteLine(
"{0} users match query", users.Count());

There’s a flaw in this method. XElement.Load method will load the whole XML file in memory and if this file is quite big, not only the query might take long time to execute but it might fail running out of memory. If we had some really large XML files we need to buffer through it instead of reading the whole contents into memory. XmlReader is a nice alternative to allowing us to have only the current record into memory which could hugely improve performance. We start by defining a User class which will be used to represent a single record:

    public class User 
{
public string Name { get; set; }
public int GroupId { get; set; }
}

Next we extend the XmlReader class with the User method:

    public static IEnumerable<User> Users(this XmlReader source)
{
while (source.Read())
{
if (source.NodeType == XmlNodeType.Element &&
source.Name
== "user")
{
int groupId;
int.TryParse(source.GetAttribute("groupid"), out groupId);
yield return new User
{
GroupId
= groupId,
Name
= source.GetAttribute("name")
};
}
}
}

And finally we can execute the query:

    using (XmlReader reader = XmlReader.Create("users.xml"))
{
var users
= from u in reader.Users()
where u.GroupId > 2
select u;
Console.WriteLine(
"{0} users match query", users.Count());
}

 

Conclusion: the second approach runs faster and uses much less memory than the first. The difference is noticeable on large XML files. So if you have to deal with large XML files be cautious when using LINQ to XML.

Last Updated on Wednesday, 19 May 2010 21:07
 

Transactional NTFS (part 1/2)

E-mail Print PDF
There are no translations available.

Transactional NTFS and DotNet

Starting from Windows Vista and Windows Server 2008, Microsoft introduced a great new feature called Transactional NTFS (TxF).

It allows developers to write file I/O functions that are guaranteed to either succeed completely or fail completely.
Unfortunately there are no classes in the .NET framework that would allows us to perform, such operations.
We need to resort to P/Invoke to use the newly introduced functions CreateTransaction, CommitTransaction, RollbackTransaction and DeleteFileTransacted.

    class Win32Native
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            int nLength;
            IntPtr lpSecurityDescriptor;
            int bInheritHandle;
        }

        [DllImport("ktmw32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern SafeFileHandle CreateTransaction(SECURITY_ATTRIBUTES securityAttributes, IntPtr guid, int options, int isolationLevel, int isolationFlags, int milliSeconds, string description);

        [DllImport("ktmw32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool CommitTransaction(SafeFileHandle transaction);

        [DllImport("ktmw32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool RollbackTransaction(SafeFileHandle transaction);

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool DeleteFileTransacted(string filename, SafeFileHandle transaction);
    }

Note that I am using SafeFileHandle in method signatures instead of IntPtr to hold unmanaged resources which guarantees that they will be properly disposed even if the AppDomain was to stop.

The next thing is to define a helper class which would allow us to easily invoke those functions:

    public class FileManager : IDisposable
    {
        private bool _commited = false;
        private SafeFileHandle _tx = null;

        public FileManager()
        {
            _tx = Win32Native.CreateTransaction(new Win32Native.SECURITY_ATTRIBUTES(), IntPtr.Zero, 0, 0, 0, 0, null);
        }

        public bool DeleteFile(string filename)
        {
            return Win32Native.DeleteFileTransacted(filename, _tx);
        }

        public void Commit()
        {
            if (Win32Native.CommitTransaction(_tx))
                _commited = true;
        }

        private void Rollback()
        {
            Win32Native.RollbackTransaction(_tx);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!_commited)
                {
                    Rollback();
                }
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
    }

Sample with delete two files

Now suppose that you need to atomically delete two files: either both files are deleted or neither of them, but you never want to have only one of the two files deleted:

    using (FileManager manager = new FileManager())
    {
        manager.DeleteFile("file1.txt");
        Console.WriteLine("file1.txt is marked for deletion in current transaction. Press Enter...");
        Console.ReadLine();

        //throw new Exception("something very bad happens here");

        manager.DeleteFile("file2.txt");
        Console.WriteLine("file2.txt is marked for deletion in current transaction.");

        manager.Commit();
    }

The method DeleteFile marks the file for deletion in the current transaction, but it will physically delete it only when the Commit method is called. Thanks to TxF and distributed transactions file and SQL operations can be performed inside the same transaction.

In part 2 we will see how to use the CreateFileTransacted function to perform atomic file read/writes.

Last Updated on Tuesday, 18 May 2010 22:09
 

Dynamic methods in .NET

E-mail Print PDF
User Rating: / 63
PoorBest 
There are no translations available.

Using reflection to invoke methods which are not known at compile time might be problematic in performance critical applications. It is roughly 2.5-3.0 times slower than direct method calls. Here’s a sample test I’ve conducted:

    class Program
    {
        [DllImport("kernel32.dll")]
        static extern void QueryPerformanceCounter(ref long ticks);

        static PropertyInfo _intProp = typeof(Foo).GetProperty("IntProp", BindingFlags.Public | BindingFlags.Instance);

        static void Main(string[] args)
        {
            Foo foo = new Foo { IntProp = 10 };
            const int COUNT = 1;
            Console.WriteLine(Measure(() => ReadPropertyWithReflection(foo), COUNT));
            Console.WriteLine(Measure(() => ReadPropertyDirectly(foo), COUNT));
        }

        static void ReadPropertyWithReflection(Foo foo)
        {
            int intProp = (int)_intProp.GetValue(foo, null);
        }

        static void ReadPropertyDirectly(Foo foo)
        {
            int intProp = foo.IntProp;
        }

        static long Measure(Action action, int count)
        {
            long startTicks = 0;
            QueryPerformanceCounter(ref startTicks);
            for (int i = 0; i < count; i++)
            {
                action();
            }
            long endTicks = 0;
            QueryPerformanceCounter(ref endTicks);
            return endTicks - startTicks;
        }

        class Foo
        {
            public int IntProp { get; set; }
        }
    }

Here are the results:

Method CPU units
Direct method invocation 796
Reflection method invocation 1986

So using reflection to perform a single property read is 2.5 slower than direct property access.

 

Dynamic methods can be used to generate and execute a method at run time, without having to declare a dynamic assembly and a dynamic type to contain the method. They are the most efficient way to generate and execute small amounts of code. Here’s an example of using DynamicMethod class to generate a getter for a given property:

    static Func<Arg0, TReturn> EmitGetter<Arg0, TReturn>(PropertyInfo propertyInfo)
    {
        MethodInfo mi = propertyInfo.GetGetMethod();
        DynamicMethod dm = new DynamicMethod(
            "_get",
            typeof(TReturn),
            new Type[] { typeof(Arg0) },
            propertyInfo.DeclaringType);

        ILGenerator il = dm.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
        il.EmitCall(OpCodes.Callvirt, mi, null);
        il.Emit(OpCodes.Ret);

        return (Func<Arg0, TReturn>)dm.CreateDelegate(typeof(Func<Arg0, TReturn>));
    }

Now using this method we can emit a getter method from a PropertyInfo at run time and execute the returned delegate:

    Func<Foo, int> getter = EmitGetter<Foo, int>(_intProp);
    Console.WriteLine(Measure(() => getter(foo), COUNT));

And here are the final results I’ve obtained using three different methods to read a property value:

Method CPU units
Direct method invocation 796
Dynamic method invocation 1190
Reflection method invocation 1986
Last Updated on Wednesday, 19 May 2010 21:12