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;
}

2010-05-20

Building SQLite.Net from source (and netmodules)

Original post from http://www.csharphacker.com/technicalblog/index.php/2009/07/05/sqlite-for-c-part-7%E2%80%93building-sqlite-net-from-source/

Thực hiện xong một vài thứ và nhận được yêu cầu merge lại thành 1 file. Mình thử dùng ILMerge như lâu giờ vẫn dùng nhưng bị báo lỗi vì System.Data.SQLite không thực hiện merge được bằng ILMerge.

Mệt quá không đóng tiền net, không làm được gì cả. Giờ mới có net lên tính tìm source của SQLite, tính chơi cách giang hồ, build 1 solution one project, copy files của mỗi project cũ thành một folder rồi build. Ai dè vẫn là đụng cái thằng này, source vừa C# vừa C++ mới nhớ netmodule, thực hiện build ra netmodule rồi link với thành 1 assembly. Sẵn tiện hỏi anh Google ra được cái link hay trên nên quote lên đây 1 bài. Để xem thử cái có cái SQLite provider nào code 100% bằng C# có lành hơn không.

2010-03-06

Zend Studio 5.5 on Windows 7


Original post http://melikedev.com/2009/11/28/zend-studio-5-5-on-windows-7/ . It is very helpful post, thank the author.





 


If you are reading this, then you have Windows 7 installed and gave Zend Studio 7.x the old college try, and found it to be quite cumbersome and annoying. After trying to adapt to Zend with Eclipse you opted to go back to Zend Studio 5.5 but found that it doesn’t work ‘out of the box’ on Windows 7. If you googled ‘Zend 5.5 Windows 7′ you probably found a response on Zend’s Forums where a savior post is located. At first read it is a bit confusing so I am going to attempt to make it a bit more clear here. Make no mistake, save for a few minor changes, the original author djvirgen (appended to by phliplip), are the true geniuses behind the work. You can find the original post @ http://www.zend.com/forums/index.php?t=msg&th=7855#21175

1. Download and install the latest JRE for Windows 7: http://java.com/en/download/manual.jsp

2. Download and install WinRAR (ensure you install with Explorer shell extension checked): http://rarlabs.com

3. Download Zend Studio 5.5.1 (you will need Zend account): http://www.zend.com/products/studio/downloads-prev

4. Extract Zend Studio 5.5.1

5. Click into ZendStudio-5_5_1 directory after extraction

6. Right mouse click on ZendStudio-5_5_1 executable, you will then see an option to ‘extract to (thanks to winrar shell extension), click it.

7. After Zend Studio 5.5.1 has been extracted click into ZendStudio-5_5_1/Windows/r
esource
8. You should see a dir named ‘jre’, rename this to ‘jre_came_with_zend’
9. Copy ‘jre6′ from your Java install directory (typically C:\Program Files (x86)\Java) into the Zend Studio extracted directory (ZendStudio-5_5_1\Windows\resource)
10. Rename ‘jre6′ to ‘jre’
11. Click up one dir so you are in ZendStudio-5_5_1/ and click on ZendStudio-5_5_1.exe
12. You will instantly know if you did not do the above steps correctly was windows 7 will inform you that you are attempting to run an app incompatible with Aero (this is because the jre packaged with zend is old, if so re-read above steps and try again)
13. If everything went fine, you should now be faced with a message “Java Runtime Environment Not Found.” Don’t worry about it, install zend as you normally would.
14. Zend Studio install should be finished, do not launch zend yet, we aren’t quite done.
15. Copy the ‘jre_came_with_zend’ directory from the original extraction folder and paste it into your Zend install directory (the one you selected during install, default is: c:\Program Files (x86)\Zend\ZendStudio-5.5.1
16. Rename the current ‘jre’ directory found in the Zend install directory to ‘jre_installed_with_zend’
17. Rename the ‘jre_came_with_zend’ to ‘jre’
18. Click into bin directory
19. Right mouse click ZDE.exe
20. Click properties
21. Click the compatibility tab
22. Click checkmark for ‘Compatibility Mode’ and select ‘Windows Vista’ from the dropdown menu
23. Click apply
24. Click ok
25. Now, with any luck you can double click on the ZDE.exe file and Zend should start normally.
Took a few hours to get all this down, hope this helps anyone else out there wanting to use Zend Studio 5.5.x vs the new Zend /w Eclipse crap. Good luck!