2010-07-10

WPF and Silverlight Framework - Dùng Prism để xây dựng ứng dụng WPF/Silverlight


Sử dụng Prism để xây dựng ứng dụng WPF/Silverlight

Khi mấy cái projects sẽ làm đụng đến WPF thì mình lại phải đi tìm hiểu trước 1 đám framework mới hy vọng chọn 1 cái khả dĩ dùng được rồi document lại, note lại những thứ cần chú ý rồi viết hướng dẫn ... Tất nhiên là try khác với use vì vậy để sử dụng được hay không cũng try cái đã. Nếu personal thì chắc cũng chẳng cần framework, mình chỉ viết vài cái toy thì túc tắc cái gì không giải quyết được lại Googling. Có thể list ra nhiều framework nhưng có vẻ nổi nhất là Caliburn và Prism hay ít nổi hơn là WAF. Đã định try thật kỹ cái Prism từ 2 năm trước :D nhưng rồi cũng xem sơ sơ. Giờ đã xem được 1 đoạn dài nên ngồi viết lại chút xíu thay đổi không khí. Đến khi xong thằng này thì sẽ coi tiếp Caliburn xem. Nhớ lại cái đám CAB/SCSF giờ cũng quăn 1 đống, hic xung quanh phát triển nhanh quá theo ko kịp.

Giới thiệu
Nối tiếp CAB/SCSF Patterns & Practices phát triển một guidance mới với mục tiêu hỗ trợ việc xây dựng ứng dụng với WPF, phiên bản đầu tiên gọi là Prism 1.0 hay Composite Application Guidance for WPF version 1.0 vào tháng 6 năm 2008. Sau đó Prism được bổ sung Silverlight gọi là Prism 2.1 hay Composite Application Guidance for WPF and Silverlight version 2.1 dành cho Silverlight 3 được ra mắt vào tháng 10 2009. Version tiếp theo là Prism 2.2 dành cho Silverlight 4 vào tháng 5 năm 2010 là một minor update của Prism 2.1 (các project chuyển qua Visual Studio 2010, target vào Silverlight 4 và update Silverlight Toolkit).

Hiện tại Prism có version 4.0 drop 3 dành cho Silverlight 4 vào ngày 1 tháng 7 năm 2010 (bài viết này là ngày 10/7/2010) với Microsoft Visual Studio 2010, Microsoft .NET Framework 4.0, Microsoft Silverlight 4, Microsoft Silverlight 4 Tools for Visual Studio 2010.

Composite Application Guidance là gì?
Là một bộ các guidance tức các chỉ dẫn bao gồm các tham khảo, ví dụ cài đặt (reference implementation), reusable library code được gọi là Composite Application Library, tài liệu, QuickStarts và hand-on labs. Đối tượng hướng tới là các software architects và developers thực hiện xây dựng các enterprise-level applications sử dụng WPF hay Silverlight.

Những loại ứng dụng này thông thường bao gồm nhiều màn hình screens, nhiều tính năng, khả năng tương tác với user linh hoạt, hiển thị dữ liệu trực quan ... Một phần quan trọng là các ứng dụng này có đặc điểm "built to last" và "built for change". Có nghĩa là các ứng dụng được mong đợi là có vòng đời tính bằng nhiều năm và có khả năng thay đổi theo những yêu cầu mới mà các yêu cầu này chưa biết trước (new and unforeseen requirements). Vì vậy ứng dụng ban đầu có thể nhỏ và được tạo thành từ những thành phần liên độc lập, kết yếu với nhau (composed of multiple independent and loosely-coupled pieces).

Các phần có trong Guidance
Guidance bao gồm các thành phần sau:
  • Composite Application Library và Unity binaries hay source code
  • StockTrader Reference Implementation là một ứng dụng mẫu dựa trên yêu cầu thực tế (real-world scenario). Đây là một tham khảo tốt để tìm hiểu những vấn đề được xác định và giải quyết như thế nào khi xây dựng composite applications.
  • 4 QuickStarts: UIComposition QS, EventAggregation QS, Modularity QS và Commanding QS là những ứng dụng nhỏ focus vào một mục tiêu cụ thể như UI composition, modularity, commanding, event aggregation ...
  • Composite Application Guidance Hands-On Lab
  • API Documentation
  • Composite Application Guidance Documentation


Stock Trader RI

Documentation
Có thể dùng diagram dưới đây để xem các thành phần trong Composite Application Guidance Documentation.
Composite Application Guidance

Trong trường hợp đã làm quen với CAB, Prism có một tài liệu riêng cho CAB developer Composite Application Guidance for CAB Developers

Home site
Home site của Prism tại địa chỉ http://compositewpf.codeplex.com/.

System requirements
Các yêu cầu phụ thuộc vào 2 vấn đề chính cần chú ý là phiên bản của Visual Studio và Silverlight.
Nếu dùng Visual Studio 2010 hay Silverlight 4 thì version của Prism tương ứng là Prism 2.2 (for Silverlight 4) - May 2010 hay Prism 4.0 Drop 3.
Nếu dùng Visual Studio 2008 thì chúng ta dùng Prism 2.1. Các yêu cầu cụ thể như sau:
·         Microsoft Visual Studio 2008 SP1
·         Microsoft .NET Framework 3.5 SP1 (.NET Framework 3.5 bao gồm WPF)
·         Microsoft Silverlight (nếu tạo ứng dụng Silverlight)
·         Microsoft Silverlight 3 Tools for Visual Studio 2008 SP1 (nếu tạo ứng dụng Silverlight)

Tham khảo
Article Patterns For Building Composite Applications With WPF - MSDN Magazine September/2008 
Prism resources at Channel9 MSDN



2010-06-17

VisualTreeHelper.HitTest và UserControl

Từ khi chuyển qua WPF nhiều thứ thay đổi, kể cả các thói quen khi code WinForms. Ví dụ trong trường hợp nào đó, mình nghĩ chức năng tương tự của WinForms nhưng WPF lại không có. Ví dụ khi click nghĩ đến bounds của control, rồi nghĩ tới 1 cái gì đó đại loại hit test thế rồi nó sẽ dẫn đến VisualTreeHelper.HitTest của WPF chẳng hạn. Nhưng mọi chuyện đâu có tuần tự tốt đẹp như vậy, sẽ có nhiều thứ để nghĩ hơn.

Ví dụ có UserControl như sau:
-->
<UserControl x:Class="TestHitTest.DemoControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="200">
    <Grid>
        <Label Content="This is a demo control" />
        <Rectangle Fill="#FF1E59C1" Stroke="Black" HorizontalAlignment="Left" Margin="18,18,0,0" VerticalAlignment="Top" Width="93" Height="48" Opacity="0.7"/>
        <Rectangle Fill="#FF83A3DA" Stroke="Black" Margin="71,29,80,0" VerticalAlignment="Top" Height="56" Opacity="0.7"/>
        <Rectangle Fill="#FF2D3441" Stroke="Black" HorizontalAlignment="Left" Margin="53,47,0,0" VerticalAlignment="Top" Width="48" Height="38" Opacity="0.7"/>
        <Rectangle Fill="#FF97A2B5" Stroke="Black" HorizontalAlignment="Left" Margin="18,75,0,0" VerticalAlignment="Top" Width="53" Height="53" Opacity="0.7"/>
        <Rectangle Fill="#FF7AA4EE" Stroke="Black" HorizontalAlignment="Left" Margin="32,96,0,114" Width="55" Opacity="0.7"/>
        <Rectangle Fill="#FF3E547A" Stroke="Black" HorizontalAlignment="Right" Margin="0,56,31,0" VerticalAlignment="Top" Width="102" Height="56" Opacity="0.7"/>
    Grid>
UserControl>

  Trong main win chứa 2 user control: 1 bên trái và một bên phải
-->
<Window x:Class="TestHitTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestHitTest" Title="Window1" Width="600" Height="300">
    <Grid x:Name="_grid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50*" />
            <ColumnDefinition Width="50*" />
        Grid.ColumnDefinitions>
        <local:DemoControl x:Name="_leftControl" Grid.Column="0" />
        <local:DemoControl x:Name="_rightControl" Grid.Column="1" />
    Grid>
Window>


Dạng như sau:












Bây giờ đơn giản khi click chúng ta cần xác định click vào user control bên phải hay bên trái và chúng ta đang đề cập VisualTreeHelper.HitTest.

Test thử HitTest
-->
namespace TestHitTest
{
    ///
    /// Interaction logic for Window1.xaml
    ///
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            MouseLeftButtonDown += new MouseButtonEventHandler(OnMouseLeftButtonDown);
        }

        void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var pt = e.GetPosition(_grid);
            var area = new EllipseGeometry(pt, 1, 1);
            Debug.WriteLine("--- Demo hit test ---");
            VisualTreeHelper.HitTest(
                _grid,
                filter =>
                {                   
                    Debug.WriteLine("Pass through filter " + filter);
                    return HitTestFilterBehavior.Continue;
                },
                result =>
                {
                    Debug.WriteLine("Pass through result " + result.VisualHit);
                    return HitTestResultBehavior.Continue;
                },
                new GeometryHitTestParameters(area)
            );
        }
    }
}

OK run và click ta được 1 cái kết quả như sau
-->
--- Demo hit test ---
Pass through filter System.Windows.Controls.Grid
Pass through filter TestHitTest.DemoControl
Pass through filter System.Windows.Controls.Border
Pass through filter System.Windows.Controls.ContentPresenter
Pass through filter System.Windows.Controls.Grid
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Controls.Label: This is a demo control
Pass through filter System.Windows.Controls.Border
Pass through result System.Windows.Controls.Border

Bây giờ chỉnh 1 chút phần filter. Như đã biết filter sẽ lọc những thứ không cần thực hiện hit test trước khi cho ra kết quả. Theo code bên dưới khi đến DemoControl thì không cần duyệt children nữa.
-->
void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var pt = e.GetPosition(_grid);
    var area = new EllipseGeometry(pt, 1, 1);
    Debug.WriteLine("--- Demo hit test ---");
    VisualTreeHelper.HitTest(
        _grid,
        filter =>
        {
            Debug.WriteLine("Pass through filter " + filter);

            if (filter.GetType().IsAssignableFrom(typeof(DemoControl)))
            {
                return HitTestFilterBehavior.ContinueSkipChildren;
            }
            else
            {
                return HitTestFilterBehavior.Continue;
            }
        },
        result =>
        {
            Debug.WriteLine("Pass through result " + result.VisualHit);
            return HitTestResultBehavior.Continue;
        },
        new GeometryHitTestParameters(area)
    );
}


Kết quả nghèo nàn như sau:

-->
--- Demo hit test ---
Pass through filter System.Windows.Controls.Grid
Pass through filter TestHitTest.DemoControl

Có nghĩa là sao,  phải chăng VisualTreeHelper.HitTest không nói rõ ràng cho lắm. Trong phần result không thèm đếm xỉa tới UserControl mà coi UserControl là một collection của các base elements. Như vậy filter chỉ dùng để hạn chế còn trong phần hit test result thì không có khái niệm user control. Trong trường hợp như vậy thì xử lý ntn?

Chúng ta sẽ thực hiện tìm parent cho hit test result
-->
public static T FindVisualParent<T>(DependencyObject child)
    where T : DependencyObject
{

    // get parent item
    DependencyObject parentObject = VisualTreeHelper.GetParent(child);

    // we’ve reached the end of the tree
    if (parentObject == null) return null;

    // check if the parent matches the type we’re looking for
    T parent = parentObject as T;

    if (parent != null)
    {
        return parent;
    }
    else
    {

        // use recursion to proceed with next level
        return FindVisualParent<T>(parentObject);
    }
}

OK lúc này hit test result sửa lại như sau (có vẻ tạm ổn):

 
-->
void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var pt = e.GetPosition(_grid);
    var area = new EllipseGeometry(pt, 1, 1);
    bool isLeft = false;
    bool isRight = false;

    Debug.WriteLine("--- Demo hit test ---");
    VisualTreeHelper.HitTest(
        _grid,
        null,
        result =>
        {
            Debug.WriteLine("Pass through result " + result.VisualHit);
            IntersectionDetail detail = ((GeometryHitTestResult)result).IntersectionDetail;

            switch (detail)
            {

                case IntersectionDetail.FullyContains:
                case IntersectionDetail.Intersects:
                case IntersectionDetail.FullyInside:
                    Debug.WriteLine(result.VisualHit.ToString());
                    DemoControl control = FindVisualParent<DemoControl>(result.VisualHit);

                    if (control != null)
                    {

                        if (control.Equals(_leftControl))
                        {
                            isLeft = true;
                            Debug.WriteLine("Click on left control");
                        }
                        else if (control.Equals(_rightControl))
                        {
                            isRight = true;
                            Debug.WriteLine("Click on right control");
                        }
                        return HitTestResultBehavior.Stop;
                    }
                    return HitTestResultBehavior.Continue;
                default:
                    return HitTestResultBehavior.Stop;
            }
        },
        new GeometryHitTestParameters(area)
    );
}



2010-05-25

Merge SQLite to executable file

 Xem bài trước http://gponster.blogspot.com/2010/05/building-sqlitenet-from-source-and.html

Sau một hồi cố merge mix-mode dll vào executable file vẫn không làm được mình đã thử dùng csharp-sqlite http://code.google.com/p/csharp-sqlite/. Tuy nhiên performace của nó khá chậm (khoảng chừng 3-5 lần khi dùng System.Data.SQLite.dll đó là theo cảm giác của mình) với lại mình gặp mới trường hợp báo assert failed có vẻ là không ổn lắm.

Benmark cho small database http://code.google.com/p/csharp-sqlite/wiki/Benchmarks

Jan 20, 2010 Updated to 3.6.22

Small Databases

# RecordsInsertingSearching 2XIteration 2XDeleting
SQLite100,0002.4s3.9s0.4s2.7s
C#-SQLite100,0005.9s4.9s0.5s5.1s
C#/SQLite2.5x1.3x1.0x1.9x

Sau một hồi search trên Google không có cách giải quyết nào khá hơn mình đành về giải pháp đầu tiên mà mình đã nghĩ. Embedded dll vào assembly resource rồi thực hiện load assembly dynamic. Data của SQLite thì cũng build dạng resource rồi thực hiện extract bình thường. Hai cái này khác nhau chút ít trong Build Action là Resource và Embedded Resource.
Để truy cập Embedded Resource dùng GetManifestResourceStream tuy nhiên phải biết chính xác tên của resource. Cách tốt nhất là xem trước tên của resource rồi thực hiện lấy resource.

-->
Assembly assembly = Assembly.GetExecutingAssembly();
string[] names = assembly.GetManifestResourceNames();
foreach (string name in names)
{
    System.Diagnostics.Debug.WriteLine(name);
}


Thêm nữa search trên stackoverflow được method sau, nhìn ok hơn là write trực tiếp ra byte
http://stackoverflow.com/questions/96732/embedding-one-dll-inside-another-as-an-embedded-resource-and-then-calling-it-from/97290#97290
-->
static byte[] StreamToBytes(Stream input)
{
    var capacity = input.CanSeek ? (int)input.Length : 0;

    using (var output = new MemoryStream(capacity))
    {
        int readLength;
        var buffer = new byte[4096];

        do
        {
            readLength = input.Read(buffer, 0, buffer.Length);
            output.Write(buffer, 0, readLength);
        } while (readLength != 0);

        return output.ToArray();
    }
}

Tiếp theo là thực hiện xử lý
-->
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolveEventHandler;
 
-->
static Assembly AssemblyResolveEventHandler(object sender, ResolveEventArgs args)
{
    Assembly result = null;

    foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
    {
        if (asm.FullName == args.Name)   //already have
        {
            result = asm;
            break;
        }
    }

    if (result == null && args.Name.Contains("System.Data.SQLite"))
    {
        var prefix = "xxx.yyy."// get full pattern or embebbed reseource
        var name = string.Format("{0}.dll", args.Name.Split(',')[0]);
        var resource = prefix + name;     // DLL have embedded
        var assembly = Assembly.GetExecutingAssembly(); // get assembly

        using (Stream input = assembly.GetManifestResourceStream(resource))
        {
            result = input != null ? LoadLibrary(name, StreamToBytes(input)) : null;
        }
    };

    return result;
}


Prefix xxx.yyy. là phần tên của resource trong phần trước assembly.GetManifestResourceNames() đã biết thông thường là MyAssemblyName.ResourceFolder.

Mới đầu thực hiện load memory sẽ bị lỗi FileLoadException Unverifiable code failed policy check. (Exception from HRESULT: 0x80131402) do SQLite là mix-mode không được build với /clr:safe nên chỉ có các load từ file. Tiếp theo đó khi write ra file với file name lấy từ Path.GetTempFileName() thay vì "System.Data.SQLite.dll" cũng bị báo lỗi tại lúc connect database
Unable to load DLL 'SQLite.Interop.DLL': The specified module could not be found

. (Exception from HRESULT: 0x8007007E)
Don't know nên chuyền thành 
-->
string tempFile = Path.GetTempFileName();
tempFile = Path.Combine(Path.GetDirectoryName(tempFile), "System.Data.SQLite.dll");

Method load library
-->
[DllImport("kernel32.dll")]
private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004;

static void MarkFileToDeleteOnReboot(string fileName)
{
    MoveFileEx(fileName, null, MOVEFILE_DELAY_UNTIL_REBOOT);
}

static Assembly LoadLibrary(string name, byte[] data)
{
    Assembly asm = null;

    try
    {
        asm = Assembly.Load(data);
    }
    catch (FileLoadException e)
    {

        if (e.Message.Contains("HRESULT: 0x80131402"))
        {

            // This is C++/CLI Mixed assembly which can only be loaded from disk, not in-memory
            string tempFile = Path.GetTempFileName();
            tempFile = Path.Combine(Path.GetDirectoryName(tempFile), name);
            File.WriteAllBytes(tempFile, data);
            asm = Assembly.LoadFile(tempFile);
            MarkFileToDeleteOnReboot(tempFile);
        }
        else
        {
            throw; // don't ignore other load errors
        }
    }

    return asm;
}

Phần data của SQLite thực hiện build với Action là Resource sẽ extract trước khi connect như sau:
-->
static string ExtractDataFile(string resource)
{
    string tempPath = Path.GetTempPath();
    string tempFile = Path.Combine(tempPath, Path.Combine("Application define here ... ", resource));
    Directory.CreateDirectory(Path.GetDirectoryName(tempFile));

    StreamResourceInfo sri = Application.GetResourceStream(
        new Uri(string.Format("ResourcesFolder/{0}", resource),
        UriKind.Relative));

    byte[] data = StreamToBytes(sri.Stream);
    sri.Stream.Close();
    sri.Stream.Dispose();

    using (FileStream fs = File.Open(tempFile, FileMode.Create))
    {
        fs.Write(data, 0, data.Length);
        fs.Close();
    }
    MarkFileToDeleteOnReboot(tempFile);
    return tempFile;
}





Các resource khác như file hay ảnh cũng thực hiện tương tự, load helper method như sau
-->
public static T LoadResource<T>(string path)
{
    T c = default(T);
    StreamResourceInfo sri = Application.GetResourceStream(new Uri(path, UriKind.Relative));

    if (sri.ContentType == "application/xaml+xml")
    {
        c = (T)XamlReader.Load(sri.Stream);
    }
    else if (sri.ContentType.IndexOf("image") >= 0)
    {
        BitmapImage bi = new BitmapImage();
        bi.BeginInit();
        bi.StreamSource = sri.Stream;
        bi.EndInit();

        if (typeof(T) == typeof(ImageSource))
        {
            c = (T)((object)bi);
        }
        else if (typeof(T) == typeof(Image))
        {

            Image img = new Image { Source = bi };
            c = (T)((object)img);
        }
    }

    sri.Stream.Close();
    sri.Stream.Dispose();

    return c;
}