How we do ASP.NET MVC

E-mail Print PDF
User Rating: / 6
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: / 2
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]

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

download32x32 wpfListViewSample03.zip [47 ko]

Last Updated on Friday, 14 May 2010 22:10
 

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

E-mail Print PDF
User Rating: / 2
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: / 2
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: / 1
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
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
 

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: / 2
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
 

LINQ to XML and reading large XML files

E-mail Print PDF
User Rating: / 3
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